diff --git a/Cargo.lock b/Cargo.lock index 36b6d42..ba3dfcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169" + [[package]] name = "addr2line" version = "0.24.2" @@ -28,6 +44,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy 0.8.26", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -52,6 +81,39 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.9.1", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[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" @@ -140,12 +202,24 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + [[package]] name = "ashpd" version = "0.9.2" @@ -548,6 +622,20 @@ 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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] [[package]] name = "byteorder" @@ -595,6 +683,32 @@ dependencies = [ "system-deps", ] +[[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", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[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 = "camino" version = "1.1.10" @@ -643,6 +757,8 @@ version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -786,10 +902,13 @@ dependencies = [ "log", "miette", "simplelog", + "softbuffer", "thiserror 1.0.69", + "tiny-skia", "tokio", "typed_shmem", "vex-v5-qemu-host", + "winit", ] [[package]] @@ -859,6 +978,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -881,6 +1010,19 @@ 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.2.1", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + [[package]] name = "core-graphics" version = "0.24.0" @@ -889,11 +1031,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", - "core-graphics-types", + "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.2.1", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -985,6 +1138,18 @@ dependencies = [ "syn 2.0.104", ] +[[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 = "darling" version = "0.20.11" @@ -1159,6 +1324,45 @@ dependencies = [ "serde", ] +[[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 = "dtoa" version = "1.0.10" @@ -1350,6 +1554,22 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fontdue" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" +dependencies = [ + "hashbrown 0.15.4", + "ttf-parser 0.21.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1605,6 +1825,16 @@ dependencies = [ "version_check", ] +[[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.1.16" @@ -1799,7 +2029,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.8", ] [[package]] @@ -1807,6 +2037,11 @@ name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -2225,6 +2460,16 @@ 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 0.3.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -2350,6 +2595,7 @@ checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" dependencies = [ "bitflags 2.9.1", "libc", + "redox_syscall 0.5.16", ] [[package]] @@ -2358,6 +2604,12 @@ 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" @@ -2441,6 +2693,15 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "memmap2" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -2714,7 +2975,7 @@ dependencies = [ "block2 0.6.1", "libc", "objc2 0.6.1", - "objc2-cloud-kit", + "objc2-cloud-kit 0.3.1", "objc2-core-data 0.3.1", "objc2-core-foundation", "objc2-core-graphics", @@ -2723,6 +2984,19 @@ dependencies = [ "objc2-quartz-core 0.3.1", ] +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-cloud-kit" version = "0.3.1" @@ -2734,6 +3008,17 @@ dependencies = [ "objc2-foundation 0.3.1", ] +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -2803,6 +3088,18 @@ dependencies = [ "objc2-foundation 0.3.1", ] +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -2855,6 +3152,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-metal" version = "0.2.2" @@ -2891,6 +3200,37 @@ dependencies = [ "objc2-foundation 0.3.1", ] +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit 0.2.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + [[package]] name = "objc2-ui-kit" version = "0.3.1" @@ -2903,6 +3243,30 @@ dependencies = [ "objc2-foundation 0.3.1", ] +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-web-kit" version = "0.3.1" @@ -2962,6 +3326,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "libredox", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -2982,6 +3355,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser 0.25.1", +] + [[package]] name = "owo-colors" version = "4.2.2" @@ -3037,7 +3419,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.16", "smallvec", "windows-targets 0.52.6", ] @@ -3188,6 +3570,26 @@ dependencies = [ "siphasher 1.0.1", ] +[[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 2.0.104", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3492,6 +3894,15 @@ 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.2.1", +] + [[package]] name = "redox_syscall" version = "0.5.16" @@ -3798,6 +4209,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + [[package]] name = "seahash" version = "4.1.0" @@ -4106,6 +4530,40 @@ 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", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "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 = "socket2" version = "0.6.0" @@ -4122,20 +4580,30 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ + "as-raw-xcb-connection", "bytemuck", "cfg_aliases 0.2.1", - "core-graphics", + "core-graphics 0.24.0", + "drm", + "fastrand", "foreign-types", "js-sys", "log", + "memmap2", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-quartz-core 0.2.2", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.16", + "rustix 0.38.44", + "tiny-xlib", "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", "web-sys", "windows-sys 0.59.0", + "x11rb", ] [[package]] @@ -4176,6 +4644,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "string_cache" version = "0.8.9" @@ -4326,7 +4800,7 @@ checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.24.0", "crossbeam-channel", "dispatch", "dlopen2", @@ -4404,7 +4878,7 @@ dependencies = [ "objc2 0.6.1", "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", - "objc2-ui-kit", + "objc2-ui-kit 0.3.1", "percent-encoding", "plist", "raw-window-handle", @@ -4605,7 +5079,7 @@ dependencies = [ "http", "jni", "objc2 0.6.1", - "objc2-ui-kit", + "objc2-ui-kit 0.3.1", "raw-window-handle", "serde", "serde_json", @@ -4817,6 +5291,45 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "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 0.8.8", + "pkg-config", + "tracing", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -5075,6 +5588,18 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "typed_shmem" version = "0.3.0" @@ -5289,6 +5814,16 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d27d92f1deaa83617d0e7f47a712982c7e9bd990797713d6d4eaca173100866f" +[[package]] +name = "vex-v5-display-simulator" +version = "0.1.0" +dependencies = [ + "fontdue", + "image", + "tiny-skia", + "vex-v5-qemu-protocol", +] + [[package]] name = "vex-v5-qemu-host" version = "0.1.0" @@ -5301,6 +5836,7 @@ dependencies = [ "miette", "thiserror 1.0.69", "tokio", + "vex-v5-display-simulator", "vex-v5-qemu-protocol", ] @@ -5490,6 +6026,28 @@ dependencies = [ "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" @@ -5502,6 +6060,32 @@ dependencies = [ "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" @@ -5521,6 +6105,7 @@ checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" dependencies = [ "dlib", "log", + "once_cell", "pkg-config", ] @@ -5534,6 +6119,16 @@ dependencies = [ "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 = "webkit2gtk" version = "2.0.1" @@ -6067,6 +6662,58 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winit" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" +dependencies = [ + "ahash 0.8.12", + "android-activity", + "atomic-waker", + "bitflags 2.9.1", + "block2 0.5.1", + "bytemuck", + "calloop", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + [[package]] name = "winnow" version = "0.5.40" @@ -6132,7 +6779,7 @@ dependencies = [ "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", - "objc2-ui-kit", + "objc2-ui-kit 0.3.1", "objc2-web-kit", "once_cell", "percent-encoding", @@ -6181,6 +6828,33 @@ dependencies = [ "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 0.8.8", + "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 = "xdg-home" version = "1.3.0" @@ -6191,6 +6865,25 @@ dependencies = [ "windows-sys 0.59.0", ] +[[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", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index e018526..68f6e31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,9 @@ members = [ "./packages/protocol", "./packages/client-cli", "./packages/client/src-tauri", + "./packages/display", ] exclude = [ - "./packages/display", # broken for now "./packages/kernel", # makes rust-analyzer really slow ] resolver = "2" diff --git a/output.png b/output.png new file mode 100644 index 0000000..724d760 Binary files /dev/null and b/output.png differ diff --git a/packages/client-cli/Cargo.toml b/packages/client-cli/Cargo.toml index e4569dc..1fc27bd 100644 --- a/packages/client-cli/Cargo.toml +++ b/packages/client-cli/Cargo.toml @@ -19,12 +19,16 @@ miette = { version = "7.2.0", features = ["fancy"] } clap = { version = "4.5.1", features = ["derive", "env"] } clap-num = "1.2.0" anyhow = "1.0.86" -bincode = "2.0.0-rc.3" +bincode = "2.0.1" vex-v5-qemu-host = { path = "../host" } thiserror = "1.0.63" log = "0.4.22" simplelog = "0.12.2" typed_shmem = "0.3.0" +winit = "0.30.12" +softbuffer = "0.4.6" +tiny-skia = "0.11.4" + [target.'cfg(any(target_os = "macos", target_os = "ios", target_os = "linux", target_os = "windows", target_os = "dragonfly", target_os = "freebsd"))'.dependencies] battery = "0.7.8" diff --git a/packages/client-cli/src/display_window.rs b/packages/client-cli/src/display_window.rs new file mode 100644 index 0000000..03bb371 --- /dev/null +++ b/packages/client-cli/src/display_window.rs @@ -0,0 +1,147 @@ +use std::{num::NonZeroU32, sync::Arc}; + +use softbuffer::Surface; +use tiny_skia::{PixmapMut, PixmapPaint, Transform}; +use tokio::{runtime::Handle, task::AbortHandle}; +use vex_v5_qemu_host::{peripherals::{display::Display, touch::Touchscreen}, protocol::{geometry::Point2, touch::{TouchData, TouchEvent}}}; +use winit::{ + application::ApplicationHandler, + dpi::PhysicalSize, + event::{ElementState, MouseButton, TouchPhase, WindowEvent}, + event_loop::ActiveEventLoop, + window::{Window, WindowId}, +}; + +pub struct DisplayWindow { + window: Option>, + task: Option, + display: Option, + touch: Touchscreen, +} + +impl DisplayWindow { + pub const fn new(display: Display, touch: Touchscreen) -> Self { + Self { + window: None, + task: None, + display: Some(display), + touch, + } + } +} + +impl ApplicationHandler for DisplayWindow { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes().with_title("Display"); + let win = Arc::new(event_loop.create_window(window_attributes).unwrap()); + win.set_resizable(false); + win.set_max_inner_size(Some(PhysicalSize::new(Display::WIDTH, Display::HEIGHT))); + win.set_min_inner_size(Some(PhysicalSize::new(Display::WIDTH, Display::HEIGHT))); + self.window = Some(win.clone()); + + let mut display = self.display.take().unwrap(); // TODO: may need to bail out if this fails instead of unwrap + let mut surface = + Surface::new(&softbuffer::Context::new(win.clone()).unwrap(), win.clone()).unwrap(); + self.task = Some( + Handle::current() + .spawn(async move { + while let Some(frame) = display.next_frame().await { + surface + .resize( + NonZeroU32::new(Display::WIDTH).unwrap(), + NonZeroU32::new(Display::HEIGHT).unwrap(), + ) + .unwrap(); + + let mut surface_buffer = surface.buffer_mut().unwrap(); + let surface_buffer_u8 = unsafe { + std::slice::from_raw_parts_mut( + surface_buffer.as_mut_ptr() as *mut u8, + surface_buffer.len() * 4, + ) + }; + + let mut pixmap = PixmapMut::from_bytes( + surface_buffer_u8, + Display::WIDTH, + Display::HEIGHT, + ) + .unwrap(); + + pixmap.draw_pixmap( + 0, + 0, + frame.as_ref(), + &PixmapPaint::default(), + Transform::identity(), + None, + ); + + // convert tiny_skia pixmap color format to softbuffer compatible format + for index in 0..(Display::WIDTH * Display::HEIGHT) as usize { + let data = pixmap.data_mut(); + surface_buffer[index] = data[index * 4 + 2] as u32 + | (data[index * 4 + 1] as u32) << 8 + | (data[index * 4] as u32) << 16; + } + + win.pre_present_notify(); + surface_buffer.present().unwrap(); + } + }) + .abort_handle(), + ); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + match event { + WindowEvent::CloseRequested => { + event_loop.exit(); + } + WindowEvent::Touch(touch) => { + Handle::current().block_on(async move { + self.touch.set_data(TouchData { + point: Point2 { + x: touch.location.x as _, + y: (touch.location.y - 32.0) as _, // TODO: determine if we need to make this work with --fullscreen once we implement that + }, + event: match touch.phase { + TouchPhase::Started | TouchPhase::Moved => TouchEvent::Press, + TouchPhase::Ended | TouchPhase::Cancelled => TouchEvent::Release, + }, + }).await; + }); + } + WindowEvent::CursorMoved { device_id: _, position } => { + Handle::current().block_on(async move { + self.touch.set_point(Point2 { + x: position.x as _, + y: (position.y - 32.0) as _, // TODO: determine if we need to make this work with --fullscreen once we implement that + }).await; + }); + } + WindowEvent::MouseInput { device_id: _, state, button: MouseButton::Left } => { + Handle::current().block_on(async move { + self.touch.set_event(match state { + ElementState::Pressed => TouchEvent::Press, + ElementState::Released => TouchEvent::Release + }).await; + }); + } + _ => (), + } + } +} + +impl Drop for DisplayWindow { + fn drop(&mut self) { + if let Some(task) = self.task.as_ref() { + task.abort(); + } + } +} diff --git a/packages/client-cli/src/main.rs b/packages/client-cli/src/main.rs index ee55909..bc3fe33 100644 --- a/packages/client-cli/src/main.rs +++ b/packages/client-cli/src/main.rs @@ -1,13 +1,6 @@ use std::{option::Option, path::PathBuf, time::Duration}; use anyhow::Context; -use log::LevelFilter; -use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode}; -use tokio::{ - io::{stdout, AsyncReadExt, AsyncWriteExt}, - process::Command, - time::sleep, -}; #[cfg(any( target_os = "macos", target_os = "ios", @@ -18,15 +11,27 @@ use tokio::{ ))] use battery::{ units::{ - electric_potential::millivolt, ratio::part_per_hundred, - thermodynamic_temperature::degree_celsius, electric_current::milliampere + electric_current::milliampere, electric_potential::millivolt, ratio::part_per_hundred, + thermodynamic_temperature::degree_celsius, }, Manager, }; +use log::LevelFilter; +use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode}; +use tokio::{ + io::{stdout, AsyncReadExt, AsyncWriteExt}, + process::Command, + time::sleep, +}; use vex_v5_qemu_host::{ brain::{Binary, Brain}, protocol::battery::BatteryData, }; +use winit::event_loop::EventLoop; + +use crate::display_window::DisplayWindow; + +mod display_window; #[cfg(debug_assertions)] const DEFAULT_KERNEL: &str = concat!( @@ -98,31 +103,25 @@ async fn main() -> anyhow::Result<()> { ) .unwrap(); - let mut brain = Brain::new(); - let peripherals = brain.peripherals.take().unwrap(); - let mut qemu = Command::new("qemu-system-arm"); + qemu.args(opt.qemu_args); if opt.gdb { qemu.args(["-S", "-s"]); } - qemu.args(opt.qemu_args); - - brain - .run_program( - qemu, - opt.kernel, - Binary { - path: opt.program, - load_addr: opt.load_addr.unwrap_or(0x03800000), - }, - opt.link.map(|link| Binary { - path: link, - load_addr: opt.link_addr.unwrap(), - }), - ) - .await - .context("Failed to start QEMU.")?; + let mut brain = Brain::new( + qemu, + opt.kernel, + Binary { + path: opt.program, + load_addr: opt.load_addr.unwrap_or(0x03800000), + }, + opt.link.map(|link| Binary { + path: link, + load_addr: opt.link_addr.unwrap(), + }), + ).unwrap(); + let peripherals = brain.peripherals.take().unwrap(); #[cfg(any( target_os = "macos", @@ -178,6 +177,13 @@ async fn main() -> anyhow::Result<()> { } }); + let _ = tokio::task::block_in_place(move || { + let event_loop = EventLoop::new().unwrap(); + let mut app = DisplayWindow::new(peripherals.display, peripherals.touch); + + event_loop.run_app(&mut app) + }); + brain.wait_for_exit().await?; Ok(()) diff --git a/packages/client/src-tauri/Cargo.toml b/packages/client/src-tauri/Cargo.toml index d20699c..aa6542c 100644 --- a/packages/client/src-tauri/Cargo.toml +++ b/packages/client/src-tauri/Cargo.toml @@ -39,7 +39,7 @@ tokio = { version = "1.39.2", features = ["full"] } vex-v5-qemu-protocol = { path = "../../protocol", features = ["serde"] } vex-v5-qemu-host = { path = "../../host" } # vex-v5-display-simulator = { version = "0.1.0", path = "../../display" } -bincode = "2.0.0-rc.3" +bincode = "2.0.1" log = "0.4.22" time = { version = "0.3.36", features = ["formatting"] } tauri-plugin-dialog = "2.0.0-beta.0" diff --git a/packages/display/Cargo.toml b/packages/display/Cargo.toml index d4c5829..1015caf 100644 --- a/packages/display/Cargo.toml +++ b/packages/display/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -ab_glyph = "0.2.28" -# fimg = { version = "0.4.45", features = ["save"], default-features = false } +fontdue = "0.9.3" image = { version = "0.25.2", default-features = false } +tiny-skia = "0.11.4" vex-v5-qemu-protocol = { version = "0.0.1", path = "../protocol" } [lints] diff --git a/packages/display/fonts/NotoMono-Regular.ttf b/packages/display/assets/NotoMono-Regular.ttf similarity index 100% rename from packages/display/fonts/NotoMono-Regular.ttf rename to packages/display/assets/NotoMono-Regular.ttf diff --git a/packages/display/assets/NotoSans-Regular.ttf b/packages/display/assets/NotoSans-Regular.ttf new file mode 100644 index 0000000..a1b8994 Binary files /dev/null and b/packages/display/assets/NotoSans-Regular.ttf differ diff --git a/packages/display/fonts/OFL.txt b/packages/display/assets/OFL.txt similarity index 100% rename from packages/display/fonts/OFL.txt rename to packages/display/assets/OFL.txt diff --git a/packages/display/assets/brain.png b/packages/display/assets/brain.png new file mode 100644 index 0000000..826f5dd Binary files /dev/null and b/packages/display/assets/brain.png differ diff --git a/packages/display/examples/error.rs b/packages/display/examples/error.rs new file mode 100644 index 0000000..8d5448e --- /dev/null +++ b/packages/display/examples/error.rs @@ -0,0 +1,60 @@ +use vex_v5_display_simulator::{ColorTheme, DisplayRenderer, TextOptions}; +use vex_v5_qemu_protocol::{ + display::{Color, Shape}, + geometry::Point2, +}; + +pub fn main() { + let mut display = DisplayRenderer::new(ColorTheme::Dark); + display.draw_header(); + + display.context.foreground_color = Color(0x8B0000); + + let container = Shape::Rectangle { + top_left: Point2 { x: 50, y: 50 }, + bottom_right: Point2 { + x: 340, + y: 120, + }, + }; + display.draw(container, false); + + display.context.foreground_color = Color(0xFFFFFF); + display.draw(container, true); + + display.draw_text( + "Memory Permission error !".to_string(), + Point2 { + x: 80, + y: 70, + }, + true, + TextOptions::default(), + ); + + display.draw_text( + "03800128".to_string(), + Point2 { + x: 80, + y: 90, + }, + true, + TextOptions::default(), + ); + + // display.context.foreground_color = Color(0x44FF44); + // let container = Shape::Rectangle { + // top_left: Point2 { + // x: 80, + // y: 70, + // }, + // bottom_right: Point2 { + // x: 81, + // y: 71, + // }, + // }; + // display.draw(container, false); + + let pix = display.render(false).unwrap(); + pix.save_png("result.png").unwrap(); +} diff --git a/packages/display/examples/shapes.rs b/packages/display/examples/shapes.rs index b6f0360..838bce1 100644 --- a/packages/display/examples/shapes.rs +++ b/packages/display/examples/shapes.rs @@ -1,10 +1,12 @@ -use std::time::Instant; - -use vex_v5_display_simulator::{DisplayRenderer, DEFAULT_BACKGROUND, DEFAULT_FOREGROUND}; -use vex_v5_qemu_protocol::{display::Shape, geometry::Point2}; +use vex_v5_display_simulator::{ColorTheme, DisplayRenderer}; +use vex_v5_qemu_protocol::{ + display::{Color, Shape}, + geometry::Point2, +}; pub fn main() { - let mut display = DisplayRenderer::new(DEFAULT_FOREGROUND, DEFAULT_BACKGROUND); + let mut display = DisplayRenderer::new(ColorTheme::Dark); + display.draw_header(); display.draw( Shape::Rectangle { @@ -14,8 +16,7 @@ pub fn main() { false, ); - display.foreground_color = [0, 0, 255]; - + display.context.foreground_color = Color(0x0000FF); display.draw( Shape::Rectangle { top_left: Point2 { x: 75, y: 75 }, @@ -23,15 +24,14 @@ pub fn main() { }, false, ); - display.draw( Shape::Circle { - center: Point2 { x: 100, y: 100 }, + center: Point2 { x: 50, y: 50 }, radius: 50, }, true, ); - display.render(false); - display.canvas.show(); + let pix = display.render(false).unwrap(); + pix.save_png("result.png").unwrap(); } diff --git a/packages/display/examples/text.rs b/packages/display/examples/text.rs index 241ff69..dab16e8 100644 --- a/packages/display/examples/text.rs +++ b/packages/display/examples/text.rs @@ -1,20 +1,18 @@ -use std::time::Instant; - -use vex_v5_display_simulator::{ - DisplayRenderer, TextOptions, DEFAULT_BACKGROUND, DEFAULT_FOREGROUND, -}; +use vex_v5_display_simulator::{ColorTheme, DisplayRenderer, TextOptions}; use vex_v5_qemu_protocol::geometry::Point2; pub fn main() { - let mut display = DisplayRenderer::new(DEFAULT_FOREGROUND, DEFAULT_BACKGROUND); + let mut display = DisplayRenderer::new(ColorTheme::Dark); + + display.draw_header(); - display.write_text( + display.draw_text( "Hello, world!".to_string(), Point2 { x: 50, y: 50 }, false, TextOptions::default(), ); - display.render(false); - display.canvas.show(); + let pix = display.render(false).unwrap(); + pix.save_png("result.png").unwrap(); } diff --git a/packages/display/src/convert.rs b/packages/display/src/convert.rs new file mode 100644 index 0000000..c549e39 --- /dev/null +++ b/packages/display/src/convert.rs @@ -0,0 +1,57 @@ +use tiny_skia::{Color, Path, PathBuilder, Rect}; +use vex_v5_qemu_protocol::display::Shape; + +pub trait ToSkia { + type Skia; + + fn to_skia(self) -> Self::Skia; +} + +const fn color_components(c: vex_v5_qemu_protocol::display::Color) -> (u8, u8, u8) { + let r = ((c.0 >> 16) & 0xFF) as u8; + let g = ((c.0 >> 8) & 0xFF) as u8; + let b = (c.0 & 0xFF) as u8; + + (r, g, b) +} + +impl ToSkia for vex_v5_qemu_protocol::display::Color { + type Skia = Color; + + fn to_skia(self) -> Self::Skia { + let (r, g, b) = color_components(self); + Color::from_rgba8(r, g, b, 0xFF) + } +} + +impl ToSkia for Shape { + type Skia = Path; + + fn to_skia(self) -> Self::Skia { + match self { + Shape::Rectangle { + top_left, + bottom_right, + } => PathBuilder::from_rect( + Rect::from_ltrb( + top_left.x as _, + top_left.y as _, + // ensure that rects of width/height 0 are drawn as hairline borders + // TODO: test if this applies to circles with r=0 + (top_left.x + (bottom_right.x - top_left.x).max(1)) as _, + (top_left.y + (bottom_right.y - top_left.y).max(1)) as _, + ) + .unwrap(), + ), + Shape::Circle { center, radius } => { + PathBuilder::from_circle(center.x as _, center.y as _, radius as _).unwrap() + } + Shape::Line { start, end } => { + let mut builder = PathBuilder::new(); + builder.move_to(start.x as f32, start.y as f32); + builder.line_to(end.x as f32, end.y as f32); + builder.finish().unwrap() + } + } + } +} diff --git a/packages/display/src/lib.rs b/packages/display/src/lib.rs index e6145a7..8d6fe35 100644 --- a/packages/display/src/lib.rs +++ b/packages/display/src/lib.rs @@ -1,65 +1,29 @@ -use std::{cell::Cell, fmt::Debug, time::Instant}; +use std::{fmt::Debug, sync::Arc, time::Duration}; -use ab_glyph::{point, Font, FontVec, Glyph, OutlinedGlyph, Point, PxScale, Rect, ScaleFont}; -pub use fimg::Pack; -use fimg::{pixels::convert::RGB, Image}; -use image::{ImageBuffer, Rgb, RgbImage}; +use fontdue::{ + layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle}, + Font, FontSettings, +}; +pub use tiny_skia::Pixmap; +use tiny_skia::{ + Color, FillRule, Mask, Paint, PathBuilder, PixmapPaint, PixmapRef, Rect, Shader, Stroke, + Transform, BlendMode, +}; use vex_v5_qemu_protocol::{ - display::{Shape, TextSize}, + display::{Color as ProtocolColor, Shape, TextSize}, geometry::Point2, }; +use crate::convert::ToSkia; + +mod convert; + /// https://internals.vexide.dev/sdk/display#foreground-and-background-colors - #c0c0ff -pub const DEFAULT_FOREGROUND: RGB = [0xc0, 0xc0, 0xff]; +pub const DEFAULT_FOREGROUND: ProtocolColor = ProtocolColor(0xc0c0ff); /// https://internals.vexide.dev/sdk/display#foreground-and-background-colors - #000000 -pub const DEFAULT_BACKGROUND: RGB = [0, 0, 0]; +pub const DEFAULT_BACKGROUND: ProtocolColor = ProtocolColor(0); /// https://internals.vexide.dev/sdk/display#code-signature - #ffffff -pub const INVERTED_BACKGROUND: RGB = [0xff, 0xff, 0xff]; - -fn draw_shape + AsRef<[u8]>>( - shape: Shape, - canvas: &mut Image, - stroke: bool, - color: RGB, -) { - match shape { - Shape::Rectangle { - top_left, - bottom_right, - } => { - let coords = (top_left.x as u32, top_left.y as u32); - let width = (bottom_right.x - top_left.x).try_into().unwrap(); - let height = (bottom_right.y - top_left.y).try_into().unwrap(); - if stroke { - canvas.r#box(coords, width, height, color); - } else { - canvas.filled_box(coords, width, height, color); - } - } - Shape::Circle { center, radius } => { - if stroke { - canvas.border_circle((center.x, center.y), radius.into(), color); - } else { - canvas.circle((center.x, center.y), radius.into(), color); - } - } - Shape::Line { start, end } => { - canvas.line((start.x, start.y), (end.x, end.y), color); - } - } -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct TextLine(pub i32); - -impl TextLine { - pub const fn coords(&self) -> Point2 { - Point2 { - x: 0, - y: self.0 * 20 + 34, - } - } -} +pub const INVERTED_BACKGROUND: ProtocolColor = ProtocolColor(0xFFFFFF); #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum V5FontSize { @@ -67,6 +31,7 @@ pub enum V5FontSize { #[default] Normal, Big, + Header, } impl From for V5FontSize { @@ -80,56 +45,68 @@ impl From for V5FontSize { } impl V5FontSize { - /// Multiplier for the X axis scale of the font. - pub const fn x_scale() -> f32 { - 0.9 - } + // /// Multiplier for the X axis scale of the font. + // pub const fn x_scale() -> f32 { + // 0.9 + // } - /// Extra spacing in pixels between characters (x-axis). - pub const fn x_spacing() -> f32 { - 1.1 - } + // /// Extra spacing in pixels between characters (x-axis). + // pub const fn x_spacing() -> f32 { + // 1.1 + // } /// Font size in pixels. - pub const fn font_size(&self) -> f32 { + pub const fn font_size(&self) -> i32 { match self { - V5FontSize::Small => 15.0, - V5FontSize::Normal => 16.0, - V5FontSize::Big => 32.0, + V5FontSize::Small => 15, + V5FontSize::Normal => 16, + V5FontSize::Big => 32, + V5FontSize::Header => 22, } } - /// Y-axis offset applied before rendering. - pub const fn y_offset(&self) -> i32 { - match self { - V5FontSize::Small => -2, - V5FontSize::Normal => -2, - V5FontSize::Big => -1, - } - } - - /// Line height of the highlighted area behind text. - pub const fn line_height(&self) -> i32 { - match self { - V5FontSize::Small => 13, - V5FontSize::Normal => 2, - V5FontSize::Big => 2, - } - } + // /// Y-axis offset applied before rendering. + // pub const fn y_offset(&self) -> i32 { + // match self { + // V5FontSize::Small => -2, + // V5FontSize::Normal => -2, + // V5FontSize::Big => -1, + // V5FontSize::Header => -2, + // } + // } + + // /// Line height of the highlighted area behind text. + // pub const fn line_height(&self) -> i32 { + // match self { + // V5FontSize::Small => 13, + // V5FontSize::Normal => 2, + // V5FontSize::Big => 2, + // V5FontSize::Header => 0, // N/A + // } + // } + + // /// Y-axis offset applied to the highlighted area behind text. + // pub const fn backdrop_y_offset(&self) -> i32 { + // match self { + // V5FontSize::Small => 2, + // V5FontSize::Normal => 0, + // V5FontSize::Big => 0, + // V5FontSize::Header => 0, // N/A + // } + // } +} - /// Y-axis offset applied to the highlighted area behind text. - pub const fn backdrop_y_offset(&self) -> i32 { - match self { - V5FontSize::Small => 2, - V5FontSize::Normal => 0, - V5FontSize::Big => 0, - } - } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum V5FontFamily { + #[default] + Monospace, + Proportional, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct TextOptions { pub size: V5FontSize, + pub family: V5FontFamily, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] @@ -139,116 +116,255 @@ pub enum RenderMode { DoubleBuffered, } -/// Blends a partially transparent foreground color with a background color. -fn blend_pixel(bg: RGB, fg: RGB, fg_alpha: f32) -> RGB { - // outputRed = (foregroundRed * foregroundAlpha) + (backgroundRed * (1.0 - - // foregroundAlpha)); +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum ColorTheme { + #[default] + Dark, + Light, +} + +impl ColorTheme { + pub const fn default_fg(&self) -> ProtocolColor { + DEFAULT_FOREGROUND + } - [ - (fg[0] as f32 * fg_alpha + bg[0] as f32 * (1.0 - fg_alpha)).round() as u8, - (fg[1] as f32 * fg_alpha + bg[1] as f32 * (1.0 - fg_alpha)).round() as u8, - (fg[2] as f32 * fg_alpha + bg[2] as f32 * (1.0 - fg_alpha)).round() as u8, - ] + pub const fn default_bg(&self) -> ProtocolColor { + match self { + Self::Dark => DEFAULT_BACKGROUND, + Self::Light => INVERTED_BACKGROUND, + } + } } +// /// Blends a partially transparent foreground color with a background color. +// fn blend_pixel(bg: Color, fg: Color, fg_alpha: f32) -> Color { +// // outputRed = (foregroundRed * foregroundAlpha) + (backgroundRed * (1.0 +// - // foregroundAlpha)); + +// [ +// (fg[0] as f32 * fg_alpha + bg[0] as f32 * (1.0 - fg_alpha)).round() +// as u8, (fg[1] as f32 * fg_alpha + bg[1] as f32 * (1.0 - +// fg_alpha)).round() as u8, (fg[2] as f32 * fg_alpha + bg[2] as f32 * +// (1.0 - fg_alpha)).round() as u8, ] +// } + pub const DISPLAY_HEIGHT: u32 = 272; pub const DISPLAY_WIDTH: u32 = 480; pub const HEADER_HEIGHT: u32 = 32; -pub const BLACK: RGB = [0, 0, 0]; -pub const WHITE: RGB = [255, 255, 255]; -pub const HEADER_BG: RGB = [0x00, 0x99, 0xCC]; +pub const HEADER_BG: ProtocolColor = ProtocolColor(0x0099CC); -type Canvas = Image, 3>; +// struct TextLayout { +// text: String, +// options: TextOptions, +// glyphs: Vec, +// /// None if the text is invisible +// bounds: Option, +// } -struct TextLayout { - text: String, - options: TextOptions, - glyphs: Vec, - /// None if the text is invisible - bounds: Option, +#[derive(Debug, Clone)] +pub struct DrawContext { + /// The display's saved foreground color. + pub foreground_color: ProtocolColor, + /// The display's saved background color. + pub background_color: ProtocolColor, + pub clip_region: Option>, } pub struct DisplayRenderer { - /// The display's saved foreground color. - pub foreground_color: RGB, - /// The display's saved background color. - pub background_color: RGB, + pub context: DrawContext, + context_stack: Vec, /// The display's image buffer. - pub canvas: Canvas, + pub canvas: Pixmap, /// When the display is in double buffered mode, this field holds the /// previous frame while the current frame is being drawn. - pub prev_canvas: Option, - user_mono: FontVec, - /// Cache for text layout calculations, to avoid re-calculating the same - /// text layout multiple times in a row. - text_layout_cache: Cell>, + pub prev_canvas: Option, + pub header_brain_image: Pixmap, + text_scratch: Pixmap, + user_mono: Font, + user_proportional: Font, + // /// Cache for text layout calculations, to avoid re-calculating the same + // /// text layout multiple times in a row. + // text_layout_cache: Cell>, } impl Debug for DisplayRenderer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DisplayRenderer") - .field("foreground_color", &self.foreground_color) - .field("background_color", &self.background_color) + .field("context", &self.context) .field("double_buffered", &self.prev_canvas.is_some()) .finish() } } impl DisplayRenderer { - pub fn new(default_fg_color: RGB, default_bg_color: RGB) -> Self { - let canvas = Image::build(DISPLAY_WIDTH, DISPLAY_HEIGHT).fill(default_bg_color); - let user_mono = - FontVec::try_from_vec(include_bytes!("../fonts/NotoMono-Regular.ttf").to_vec()) - .unwrap(); + pub fn new(theme: ColorTheme) -> Self { + let mut canvas = Pixmap::new(DISPLAY_WIDTH, DISPLAY_HEIGHT).unwrap(); + let text_scratch = canvas.clone(); + let mut mask = Mask::new(DISPLAY_WIDTH, DISPLAY_HEIGHT).unwrap(); + + let monospace = Font::from_bytes( + include_bytes!("../assets/NotoMono-Regular.ttf") as &[u8], + FontSettings::default(), + ) + .unwrap(); + + let proportional = Font::from_bytes( + include_bytes!("../assets/NotoSans-Regular.ttf") as &[u8], + FontSettings::default(), + ) + .unwrap(); + + canvas.fill(theme.default_bg().to_skia()); + + let path = PathBuilder::from_rect( + Rect::from_ltrb( + 0.0, + HEADER_HEIGHT as f32, + DISPLAY_WIDTH as f32, + DISPLAY_HEIGHT as f32, + ) + .unwrap(), + ); + mask.fill_path(&path, FillRule::EvenOdd, false, Transform::identity()); Self { - foreground_color: default_fg_color, - background_color: default_bg_color, - user_mono, + context: DrawContext { + foreground_color: theme.default_fg(), + background_color: theme.default_bg(), + clip_region: Some(Arc::new(mask)), + }, + context_stack: vec![], + user_mono: monospace, + user_proportional: proportional, + header_brain_image: Pixmap::decode_png(include_bytes!("../assets/brain.png")).unwrap(), canvas, prev_canvas: None, - text_layout_cache: Cell::default(), + text_scratch, + // text_layout_cache: Cell::default(), } } + pub fn save(&mut self) { + self.context_stack.push(self.context.clone()); + } + + pub fn restore(&mut self) { + if let Some(ctx) = self.context_stack.pop() { + self.context = ctx; + } + } + + pub fn draw_header(&mut self, name: String, time: Duration) { + self.save(); + + // Background + self.context.foreground_color = HEADER_BG; + self.context.clip_region = None; + self.draw( + Shape::Rectangle { + top_left: Point2 { x: 0, y: 0 }, + bottom_right: Point2 { + x: DISPLAY_WIDTH as _, + y: HEADER_HEIGHT as _, + }, + }, + false, + ); + + // Program name + self.context.foreground_color = ProtocolColor(0); + self.draw_text( + name.to_string(), + Point2 { x: 8, y: 7 }, + true, + TextOptions { + size: V5FontSize::Header, + family: V5FontFamily::Proportional, + }, + ); + + // Timer + let secs = time.as_secs(); + self.draw_text( + format!("{}:{:02}", secs / 60, secs % 60), + Point2 { x: 247, y: 7 }, + true, + TextOptions { + size: V5FontSize::Header, + family: V5FontFamily::Monospace, + }, + ); + + // Brain icon + self.canvas.draw_pixmap( + 441, + -1, + self.header_brain_image.as_ref(), + &PixmapPaint::default(), + Transform::identity(), + None, + ); + + // Battery icon + self.context.foreground_color = ProtocolColor(0); + self.draw(Shape::Rectangle { + top_left: Point2 { x: 453, y: 23 }, + bottom_right: Point2 { x: 466, y: 32 }, + }, false); + self.draw( + Shape::Rectangle { + top_left: Point2 { x: 466, y: 26 }, + bottom_right: Point2 { x: 468, y: 29 }, + }, + false, + ); + + self.context.foreground_color = ProtocolColor(0x93c83f); + self.draw(Shape::Rectangle { + top_left: Point2 { x: 454, y: 24 }, + bottom_right: Point2 { x: 465, y: 31 }, + }, false); + + self.restore(); + } + /// Copies a buffer of pixels to the display. pub fn draw_buffer( &mut self, buf: &[u8], top_left: Point2, - bot_right: Point2, + bottom_right: Point2, stride: usize, ) { - let mut y = top_left.y; - for row in buf.chunks(stride * 4) { - if y > bot_right.y { - break; - } + let width = (bottom_right.x - top_left.x) as u32; + let height = (bottom_right.y - top_left.y) as u32; - let mut x = top_left.x; - for pixel in row.chunks(4) { - let color = RGB::unpack(u32::from_le_bytes(pixel[0..4].try_into().unwrap())); - if x >= 0 - && x < self.canvas.width() as i32 - && y >= 0 - && y < self.canvas.height() as i32 - { - // I didn't see a safe version of this...? - // SAFETY: bounds are checked - unsafe { - self.canvas - .set_pixel(x.try_into().unwrap(), y.try_into().unwrap(), color) - }; - } - x += 1; - } - y += 1; + if height == 0 || width == 0 { + return; + } + + if stride as u32 != width { + unimplemented!("stride != width") } + + let pixmap = PixmapRef::from_bytes(buf, width, height).expect("nonzero"); + + self.canvas.draw_pixmap( + top_left.x, + top_left.y, + pixmap, + &PixmapPaint { + blend_mode: BlendMode::SourceOver, + ..Default::default() + }, + Transform::identity(), + None, + ); } /// Returns the next display frame, if one is available. - pub fn render(&mut self, explicitly_requested: bool) -> Option { + pub fn render(&mut self, explicitly_requested: bool) -> Option { if explicitly_requested { // Save the current state of the display so we can continue // showing it as the next frame is being drawn. The existence @@ -258,9 +374,7 @@ impl DisplayRenderer { return None; } - let frame = self.prev_canvas.as_ref().unwrap_or(&self.canvas); - // frame.clone().show(); - RgbImage::from_raw(frame.width(), frame.height(), Vec::from(&**frame.buffer())) + Some(self.prev_canvas.as_ref().unwrap_or(&self.canvas).clone()) } pub const fn render_mode(&self) -> RenderMode { @@ -279,91 +393,116 @@ impl DisplayRenderer { /// Erases the display by filling it with the default background color. pub fn erase(&mut self) { - self.canvas - .filled_box((0, 0), DISPLAY_WIDTH, DISPLAY_HEIGHT, self.background_color); + self.canvas.fill(self.context.background_color.to_skia()); + } + + fn fg_paint(&self) -> Paint<'static> { + Paint { + shader: Shader::SolidColor(self.context.foreground_color.to_skia()), + anti_alias: false, + ..Default::default() + } } /// Draws or strokes a shape on the display, using the current foreground /// color. pub fn draw(&mut self, shape: Shape, stroke: bool) { - draw_shape(shape, &mut self.canvas, stroke, self.foreground_color) - } - - /// Removes the last text layout from the cache and returns it if it matches - /// the given text and options. - fn check_layout_cache(&self, text: &str, options: TextOptions) -> Option { - let layout = self.text_layout_cache.take()?; - if text == layout.text && options == layout.options { - Some(layout) + let path = shape.to_skia(); + + if stroke || matches!(shape, Shape::Line { start: _, end: _ }) { + self.canvas.stroke_path( + &path, + &self.fg_paint(), + &Stroke::default(), + Transform::identity(), + self.context.clip_region.as_deref(), + ); } else { - None - } - } - - /// Returns the layout for the given text, using the given options. - /// - /// May either return cached glyphs or calculate them when called. - fn layout_for(&self, text: String, options: TextOptions) -> TextLayout { - if let Some(layout) = self.check_layout_cache(&text, options) { - return layout; - } - - let scale = PxScale { - y: options.size.font_size(), - // V5's version of the Noto Mono font is slightly different - // than the one bundled with the simulator, so we have to apply - // an scale on the X axis and later move the characters further apart. - x: options.size.font_size() * V5FontSize::x_scale(), - }; - - let scale_font = self.user_mono.as_scaled(scale); - let mut glyphs = Vec::new(); - - layout_glyphs(scale_font, &text, V5FontSize::x_spacing(), &mut glyphs); - - let outlined: Vec = glyphs - .into_iter() - // removes any invisible characters - .filter_map(|g| self.user_mono.outline_glyph(g)) - .collect(); - - let bounds = outlined - .iter() - .map(|g| g.px_bounds()) - .reduce(|mut b, next| { - b.min.x = b.min.x.min(next.min.x); - b.max.x = b.max.x.max(next.max.x); - b.min.y = b.min.y.min(next.min.y); - b.max.y = b.max.y.max(next.max.y); - b - }); - - TextLayout { - text, - options, - glyphs: outlined, - bounds, + self.canvas.fill_path( + &path, + &self.fg_paint(), + FillRule::Winding, + Transform::identity(), + self.context.clip_region.as_deref(), + ); } } - /// Calculates the shape of the area behind a text layout, so that it can be - /// drawn on top of a background color. - fn calculate_text_background( - glyphs: &TextLayout, - coords: Point2, - font_size: V5FontSize, - ) -> Option { - glyphs.bounds.map(|size| Shape::Rectangle { - top_left: Point2 { - x: size.min.x as i32 + coords.x - 1, - y: coords.y + font_size.backdrop_y_offset(), - }, - bottom_right: Point2 { - x: size.max.x as i32 + coords.x + 1, - y: coords.y + font_size.backdrop_y_offset() + font_size.line_height() - 1, - }, - }) - } + // /// Removes the last text layout from the cache and returns it if it matches + // /// the given text and options. + // fn check_layout_cache(&self, text: &str, options: TextOptions) -> + // Option { let layout = self.text_layout_cache.take()?; + // if text == layout.text && options == layout.options { + // Some(layout) + // } else { + // None + // } + // } + + // /// Returns the layout for the given text, using the given options. + // /// + // /// May either return cached glyphs or calculate them when called. + // fn layout_for(&self, text: String, options: TextOptions) -> TextLayout { + // if let Some(layout) = self.check_layout_cache(&text, options) { + // return layout; + // } + + // let scale = PxScale { + // y: options.size.font_size(), + // // V5's version of the Noto Mono font is slightly different + // // than the one bundled with the simulator, so we have to apply + // // an scale on the X axis and later move the characters further + // apart. x: options.size.font_size() * V5FontSize::x_scale(), + // }; + + // let scale_font = self.user_mono.as_scaled(scale); + // let mut glyphs = Vec::new(); + + // layout_glyphs(scale_font, &text, V5FontSize::x_spacing(), &mut glyphs); + + // let outlined: Vec = glyphs + // .into_iter() + // // removes any invisible characters + // .filter_map(|g| self.user_mono.outline_glyph(g)) + // .collect(); + + // let bounds = outlined + // .iter() + // .map(|g| g.px_bounds()) + // .reduce(|mut b, next| { + // b.min.x = b.min.x.min(next.min.x); + // b.max.x = b.max.x.max(next.max.x); + // b.min.y = b.min.y.min(next.min.y); + // b.max.y = b.max.y.max(next.max.y); + // b + // }); + + // TextLayout { + // text, + // options, + // glyphs: outlined, + // bounds, + // } + // } + + // /// Calculates the shape of the area behind a text layout, so that it can be + // /// drawn on top of a background color. + // fn calculate_text_background( + // glyphs: &TextLayout, + // coords: Point2, + // font_size: V5FontSize, + // ) -> Option { + // glyphs.bounds.map(|size| Shape::Rectangle { + // top_left: Point2 { + // x: size.min.x as i32 + coords.x - 1, + // y: coords.y + font_size.backdrop_y_offset(), + // }, + // bottom_right: Point2 { + // x: size.max.x as i32 + coords.x + 1, + // y: coords.y + font_size.backdrop_y_offset() + + // font_size.line_height() - 1, }, + // }) + // } /// Writes text to the display at a given coordinate. Use /// [`TextLine::coords`] to convert a line number to a coordinate for @@ -376,102 +515,163 @@ impl DisplayRenderer { /// * `transparent`: Whether the text should be written without a /// background. /// * `options`: The options to use when rendering the text. - pub fn write_text( + pub fn draw_text( &mut self, text: String, mut coords: Point2, - transparent: bool, + _transparent: bool, options: TextOptions, ) { if text.is_empty() { return; } - // The V5's text is all offset vertically from ours, so this adjustment makes it - // consistent. - coords.y += options.size.y_offset(); + let px = options.size.font_size(); - let fg = self.foreground_color; - let layout = self.layout_for(text, options); - - if !transparent { - if let Some(backdrop) = Self::calculate_text_background(&layout, coords, options.size) { - draw_shape(backdrop, &mut self.canvas, false, self.background_color); + let font = match options.family { + V5FontFamily::Monospace => &self.user_mono, + V5FontFamily::Proportional => &self.user_proportional, + }; + // Fix gap above text + let fullheight_metrics = font.metrics('M', px as f32); + let font_metrics = font.horizontal_line_metrics(px as f32).unwrap(); + let y_offset = fullheight_metrics.height as i32 - font_metrics.ascent as i32; + coords.y += y_offset; + + let mut layout = Layout::new(CoordinateSystem::PositiveYDown); + let fonts = &[font]; + + layout.reset(&LayoutSettings { + x: coords.x as f32, + y: coords.y as f32, + wrap_hard_breaks: false, + ..LayoutSettings::default() + }); + + layout.append(fonts, &TextStyle::new(&text, px as f32, 0)); + + self.text_scratch.fill(Color::TRANSPARENT); + let pixels = self.text_scratch.pixels_mut(); + + for glyph in layout.glyphs() { + if !glyph.char_data.rasterize() { + continue; } - } - for glyph in layout.glyphs.iter() { - let bounds = glyph.px_bounds(); - // Draw the glyph into the image per-pixel - glyph.draw(|mut x, mut y, alpha| { - // Apply offsets to make the coordinates image-relative, not text-relative - x += bounds.min.x as u32 + coords.x as u32; - y += bounds.min.y as u32 + coords.y as u32; + let font = fonts[glyph.font_index]; + let (metrics, bitmap) = font.rasterize_config(glyph.key); + + for rel_y in 0..metrics.height { + for rel_x in 0..metrics.width { + let coverage = bitmap[rel_x + rel_y * metrics.width] as f32 / u8::MAX as f32; + let color = { + let mut fg = self.context.foreground_color.to_skia(); + let alpha = fg.alpha() * coverage; + let alpha = -(alpha - 1.0).powi(2) + 1.0; + fg.set_alpha(alpha); + fg + }; - if !(x < self.canvas.width() && y < self.canvas.height()) { - return; - } + let x = glyph.x as usize + rel_x; + let y = glyph.y as usize + rel_y; - // I didn't find a safe version of pixel and set_pixel. - // SAFETY: Pixel bounds are checked. - unsafe { - let old_pixel = self.canvas.pixel(x, y); - - self.canvas.set_pixel( - x, - y, - // Taking this power seems to make the alpha blending look better; - // otherwise it's not heavy enough. - blend_pixel(old_pixel, fg, alpha.powf(0.4).clamp(0.0, 1.0)), - ); + if let Some(pixel) = pixels.get_mut(x + y * DISPLAY_WIDTH as usize) { + *pixel = color.to_color_u8().premultiply(); + } } - }); + } } - // Add (or re-add) the laid-out glyphs to the cache so they can be used later. - self.text_layout_cache.set(Some(layout)); + self.canvas.draw_pixmap( + 0, + 0, + self.text_scratch.as_ref(), + &PixmapPaint::default(), + Transform::identity(), + self.context.clip_region.as_deref(), + ); + + // let fg = self.context.foreground_color; + + // let layout = self.layout_for(text, options); + + // if !transparent { + // if let Some(backdrop) = Self::calculate_text_background(&layout, + // coords, options.size) { draw_shape(backdrop, &mut + // self.canvas, false, self.background_color); } + // } + + // for glyph in layout.glyphs.iter() { + // let bounds = glyph.px_bounds(); + // // Draw the glyph into the image per-pixel + // glyph.draw(|mut x, mut y, alpha| { + // // Apply offsets to make the coordinates image-relative, not + // text-relative x += bounds.min.x as u32 + coords.x as + // u32; y += bounds.min.y as u32 + coords.y as u32; + + // if !(x < self.canvas.width() && y < self.canvas.height()) { + // return; + // } + + // // I didn't find a safe version of pixel and set_pixel. + // // SAFETY: Pixel bounds are checked. + // unsafe { + // let old_pixel = self.canvas.pixel(x, y); + + // self.canvas.set_pixel( + // x, + // y, + // // Taking this power seems to make the alpha blending + // look better; // otherwise it's not heavy + // enough. blend_pixel(old_pixel, fg, + // alpha.powf(0.4).clamp(0.0, 1.0)), ); + // } + // }); + + // Add (or re-add) the laid-out glyphs to the cache so they can be used + // later.self.text_layout_cache.set(Some(layout)); } - /// Calculates how big a string will be when rendered. - /// - /// Caches the result so that the same text and options don't have to be - /// calculated multiple times in a row. - pub fn calculate_string_size(&self, text: String, options: TextOptions) -> Point { - let layout = self.layout_for(text, options); - let size = layout.bounds; - self.text_layout_cache.set(Some(layout)); - size.unwrap_or_default().max - } + // /// Calculates how big a string will be when rendered. + // /// + // /// Caches the result so that the same text and options don't have to be + // /// calculated multiple times in a row. + // pub fn calculate_string_size(&self, text: String, options: TextOptions) -> + // Point { let layout = self.layout_for(text, options); + // let size = layout.bounds; + // self.text_layout_cache.set(Some(layout)); + // size.unwrap_or_default().max + // } } -// mostly based on the example from ab_glyph -pub fn layout_glyphs(font: SF, text: &str, x_spacing: f32, target: &mut Vec) -where - F: Font, - SF: ScaleFont, -{ - let mut caret = point(0.0, font.ascent()); - let mut last_glyph: Option = None; - - for mut c in text.chars() { - // Vex replaces newlines with a period - // Assuming here it's for all control characters - if c.is_control() { - c = '.'; - } - - // Render and kern - let mut glyph = font.scaled_glyph(c); - if let Some(previous) = last_glyph.take() { - caret.x += font.kern(previous.id, glyph.id); - } - glyph.position = caret; - - // Advance to the next position - last_glyph = Some(glyph.clone()); - caret.x += font.h_advance(glyph.id); - caret.x += x_spacing; - - target.push(glyph); - } -} +// // mostly based on the example from ab_glyph +// pub fn layout_glyphs(font: SF, text: &str, x_spacing: f32, target: +// &mut Vec) where +// F: Font, +// SF: ScaleFont, +// { +// let mut caret = point(0.0, font.ascent()); +// let mut last_glyph: Option = None; + +// for mut c in text.chars() { +// // Vex replaces newlines with a period +// // Assuming here it's for all control characters +// if c.is_control() { +// c = '.'; +// } + +// // Render and kern +// let mut glyph = font.scaled_glyph(c); +// if let Some(previous) = last_glyph.take() { +// caret.x += font.kern(previous.id, glyph.id); +// } +// glyph.position = caret; + +// // Advance to the next position +// last_glyph = Some(glyph.clone()); +// caret.x += font.h_advance(glyph.id); +// caret.x += x_spacing; + +// target.push(glyph); +// } +// } diff --git a/packages/host/Cargo.toml b/packages/host/Cargo.toml index 681b07a..46b4c6b 100644 --- a/packages/host/Cargo.toml +++ b/packages/host/Cargo.toml @@ -17,9 +17,9 @@ vex-v5-qemu-protocol = { path = "../protocol" } tokio = { version = "1.39.3", features = ["full"] } miette = "7.2.0" thiserror = "1.0.63" -bincode = "2.0.0-rc.3" +bincode = "2.0.1" cobs = "0.2.3" log = "0.4.22" image = { version = "0.25.2", default-features = false } -# vex-v5-display-simulator = { version = "0.1.0", path = "../display" } +vex-v5-display-simulator = { version = "0.1.0", path = "../display" } bytemuck = "1.17.0" diff --git a/packages/host/src/brain.rs b/packages/host/src/brain.rs index 032a7a8..cf298b3 100644 --- a/packages/host/src/brain.rs +++ b/packages/host/src/brain.rs @@ -1,22 +1,22 @@ use std::{ - io, option::Option, path::PathBuf, process::{ExitStatus, Stdio}, sync::Arc, time::Instant, vec::Vec + io, option::Option, path::PathBuf, process::{ExitStatus, Stdio}, sync::Arc, time::Duration, vec::Vec }; use tokio::{ - io::AsyncWriteExt, - process::Command, + io::{AsyncReadExt, AsyncWriteExt}, + process::{Child, ChildStdin, ChildStdout, Command}, sync::{ - mpsc::{self, Sender}, - Barrier, Mutex, + mpsc::{self, Receiver, Sender}, + Barrier, Mutex, RwLock, }, - task::AbortHandle, + task::AbortHandle, time::sleep, }; -use vex_v5_qemu_protocol::{display::DrawCommand, DisplayCommand, HostBoundPacket, KernelBoundPacket, SmartPortCommand}; +use vex_v5_qemu_protocol::{DisplayCommand, HostBoundPacket, KernelBoundPacket, SmartPortCommand}; use crate::{ - connection::QemuConnection, peripherals::{ - battery::Battery, display::Display, smartport::SmartPort, usb::Usb, Peripherals, + battery::Battery, display::Display, smartport::SmartPort, touch::Touchscreen, usb::Usb, + Peripherals, }, }; @@ -28,18 +28,64 @@ pub struct Binary { pub struct Brain { pub peripherals: Option, - connection: Arc>>, - task: AbortHandle, - barrier: Arc, + tx_task: AbortHandle, + rx_task: AbortHandle, + qemu: Arc>, } impl Brain { #[allow(clippy::new_without_default)] - pub fn new() -> Self { - let connection = Arc::new(Mutex::new(None)); - let barrier = Arc::new(Barrier::new(2)); + pub fn new( + mut qemu_command: Command, + kernel: PathBuf, + main_binary: Binary, + linked_binary: Option, + ) -> io::Result { + let link_addr: u32 = linked_binary.clone().map_or(0, |v| v.load_addr); + let qemu_command = qemu_command + .args(["-machine", "xilinx-zynq-a9,memory-backend=mem"]) + .args(["-cpu", "cortex-a9"]) + .args(["-object", "memory-backend-ram,id=mem,size=256M"]) + .args([ + "-device", + &format!("loader,addr=0x200,data={},data-len=4,cpu-num=0", link_addr), + ]) + .args([ + "-device", + &format!("loader,file={},addr=0x100000,cpu-num=0", kernel.display()), + ]) + .args([ + "-device", + &format!( + "loader,file={},force-raw=on,addr={}", + main_binary.path.display(), + main_binary.load_addr + ), + ]) + .args(["-display", "none"]) + .args([ + "-semihosting", + "-semihosting-config", + "enable=on,target=native", + ]) + .args(["-chardev", "stdio,id=uart"]) + .args(["-serial", "null"]) + .args(["-serial", "chardev:uart"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .kill_on_drop(true); + + if let Some(linked_binary) = linked_binary { + qemu_command.arg("-device"); + qemu_command.arg(format!( + "loader,file={},force-raw=on,addr={}", + linked_binary.path.display(), + linked_binary.load_addr + )); + } - let (peripherals_tx, peripherals_rx) = mpsc::channel::(1024); + let (peripherals_tx, mut peripherals_rx) = mpsc::channel::(1024); // Each of these channels represents a serial line for device commands from the // kernel to a smartport. Commands sent to devices by the kernel are @@ -69,74 +115,86 @@ impl Brain { let (usb_tx, usb_rx) = mpsc::channel::>(1); let (display_tx, display_rx) = mpsc::channel::(1); - Self { - connection: connection.clone(), - barrier: barrier.clone(), - task: tokio::task::spawn(async move { - let mut start = std::time::Instant::now(); - let mut peripherals_rx = peripherals_rx; - let smartport_senders: [Sender; 21] = [ + let mut qemu = qemu_command.spawn()?; + let mut qemu_stdin = qemu.stdin.take().unwrap(); + let mut qemu_stdout = qemu.stdout.take().unwrap(); + + let qemu = Arc::new(Mutex::new(qemu)); + + Ok(Self { + qemu: qemu.clone(), + tx_task: tokio::task::spawn(async move { + loop { + if let Some(packet) = peripherals_rx.recv().await { + let encoded = + bincode::encode_to_vec(packet, bincode::config::standard()).unwrap(); + let mut bytes = Vec::new(); + + bytes.extend((encoded.len() as u32).to_le_bytes()); + bytes.extend(encoded); + + qemu_stdin.write_all(&bytes).await.unwrap(); + } + } + }) + .abort_handle(), + rx_task: tokio::spawn(async move { + let smartport_senders = [ port_1_tx, port_2_tx, port_3_tx, port_4_tx, port_5_tx, port_6_tx, port_7_tx, port_8_tx, port_9_tx, port_10_tx, port_11_tx, port_12_tx, port_13_tx, port_14_tx, port_15_tx, port_16_tx, port_17_tx, port_18_tx, port_19_tx, port_20_tx, port_21_tx, ]; - // This is the event loop that facilitates packet exchange with the QEMU - // process over the simulator protocol. It has (roughly) two jobs: - // - Receive packets from peripherals and send them to the kernel. - // - Receive packets from the kernel and forward them to peripherals. loop { - let mut connection_guard = connection.lock().await; - if let Some(connection) = connection_guard.as_mut() { - // Send the latest packet from peripherals to the kernel. - if let Ok(peripherals_packet) = peripherals_rx.try_recv() { - connection.send_packet(peripherals_packet).await.unwrap(); - } + let incoming_packet: HostBoundPacket = { + let packet_size = qemu_stdout.read_u32_le().await.unwrap() as usize; + let mut buf = vec![0u8; packet_size]; - // If a child process is running, then receive kernel packets and forward - // them to a respective peripheral's `Receiver`. - match connection.recv_packet().await.unwrap() { - // Forward sent data to usb peripheral. - HostBoundPacket::UsbSerial(data) => { - _ = usb_tx.send(data).await; - } + qemu_stdout.read_exact(&mut buf).await.unwrap(); - // Kernel debugging stuff (logs mainly) goes to stderr - HostBoundPacket::KernelSerial(data) => { - let mut stderr = tokio::io::stderr(); - stderr.write_all(&data).await.unwrap(); - stderr.flush().await.unwrap(); - } + bincode::decode_from_slice(&buf, bincode::config::standard()) + .unwrap() + .0 + }; - // Doesn't matter for now. - HostBoundPacket::CodeSignature(_) => {} + match incoming_packet { + // Forward sent data to usb peripheral. + HostBoundPacket::UsbSerial(data) => { + _ = usb_tx.send(data).await; + } - // Kill QEMU child process when kernel requests exit. - HostBoundPacket::ExitRequest(code) => { - connection.child.kill().await.unwrap(); - *connection_guard = None; - log::info!("Kernel exited with code {code}."); - } + // Kernel debugging stuff (logs mainly) goes to stderr + HostBoundPacket::KernelSerial(data) => { + let mut stderr = tokio::io::stderr(); + stderr.write_all(&data).await.unwrap(); + stderr.flush().await.unwrap(); + } - // The kernel has sent a device command packet to a specific smartport, - // so we must forward that packet to the respective smartport's - // receiver. - HostBoundPacket::SmartPortCommand { port, command } => { - if let Some(port_tx) = smartport_senders.get(port as usize) { - // We ignore errors if the packet send fails, since it means - // the user has dropped the smartport and by extension the - // receiver, meaning there's nothing to send to. - _ = port_tx.send(command).await; - } - } + // Doesn't matter for now. + HostBoundPacket::CodeSignature(_) => {} + + // Kill QEMU child process when kernel requests exit. + HostBoundPacket::ExitRequest(code) => { + qemu.lock().await.kill().await.unwrap(); + log::info!("Kernel exited with code {code}."); + } - HostBoundPacket::DisplayCommand { command } => { - _ = display_tx.send(command).await; + // The kernel has sent a device command packet to a specific smartport, + // so we must forward that packet to the respective smartport's + // receiver. + HostBoundPacket::SmartPortCommand { port, command } => { + if let Some(port_tx) = smartport_senders.get(port as usize) { + // We ignore errors if the packet send fails, since it means + // the user has dropped the smartport and by extension the + // receiver, meaning there's nothing to send to. + _ = port_tx.send(command).await; } } - } else { - barrier.wait().await; + + HostBoundPacket::DisplayCommand { command } => { + _ = display_tx.send(command).await; + } } } }) @@ -168,93 +226,24 @@ impl Brain { port_21: SmartPort::new(20, peripherals_tx.clone(), port_21_rx), display: Display::new(peripherals_tx.clone(), display_rx), + touch: Touchscreen::new(peripherals_tx.clone()), }), - } - } - - pub async fn run_program( - &mut self, - mut qemu_command: Command, - kernel: PathBuf, - main_binary: Binary, - linked_binary: Option, - ) -> io::Result<()> { - let link_addr: u32 = linked_binary.clone().map_or(0, |v| v.load_addr); - let qemu_command = qemu_command - .args(["-machine", "xilinx-zynq-a9,memory-backend=mem"]) - .args(["-cpu", "cortex-a9"]) - .args(["-object", "memory-backend-ram,id=mem,size=256M"]) - .args([ - "-device", - &format!("loader,addr=0x200,data={},data-len=4,cpu-num=0", link_addr), - ]) - .args([ - "-device", - &format!("loader,file={},addr=0x100000,cpu-num=0", kernel.display()), - ]) - .args([ - "-device", - &format!( - "loader,file={},force-raw=on,addr={}", - main_binary.path.display(), - main_binary.load_addr - ), - ]) - .args(["-display", "none"]) - .args([ - "-semihosting", - "-semihosting-config", - "enable=on,target=native", - ]) - .args(["-chardev", "stdio,id=uart"]) - .args(["-serial", "null"]) - .args(["-serial", "chardev:uart"]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) - .kill_on_drop(true); - - if let Some(linked_binary) = linked_binary { - qemu_command.arg("-device"); - qemu_command.arg(format!( - "loader,file={},force-raw=on,addr={}", - linked_binary.path.display(), - linked_binary.load_addr - )); - } - - let mut child = qemu_command.spawn()?; - - self.barrier.wait().await; - *self.connection.lock().await = Some(QemuConnection { - stdin: child.stdin.take().unwrap(), - stdout: child.stdout.take().unwrap(), - child, - }); - - Ok(()) - } - - pub async fn kill_program(&mut self) -> io::Result<()> { - if let Some(mut connection) = self.connection.lock().await.take() { - connection.child.kill().await - } else { - Ok(()) - } + }) } pub async fn wait_for_exit(&mut self) -> io::Result> { - while let Some(connection) = self.connection.lock().await.as_mut() { - if let Some(status) = connection.child.try_wait()? { + loop { + if let Some(status) = self.qemu.lock().await.try_wait()? { return Ok(Some(status)); } + sleep(Duration::from_millis(10)).await; } - Ok(None) } } impl Drop for Brain { fn drop(&mut self) { - self.task.abort(); + self.tx_task.abort(); + self.rx_task.abort(); } } diff --git a/packages/host/src/connection.rs b/packages/host/src/connection.rs deleted file mode 100644 index 8f3192c..0000000 --- a/packages/host/src/connection.rs +++ /dev/null @@ -1,54 +0,0 @@ -use bincode::error::{DecodeError, EncodeError}; -use miette::Diagnostic; -use thiserror::Error; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - process::{Child, ChildStdin, ChildStdout}, -}; -use vex_v5_qemu_protocol::{HostBoundPacket, KernelBoundPacket}; - -#[derive(Debug)] -pub struct QemuConnection { - pub child: Child, - pub stdin: ChildStdin, - pub stdout: ChildStdout, -} - -#[derive(Error, Diagnostic, Debug)] -pub enum ConnectionError { - #[error(transparent)] - #[diagnostic(code(simulator::io_error))] - Io(#[from] std::io::Error), - - #[error(transparent)] - #[diagnostic(code(simulator::encode_error))] - Encode(#[from] EncodeError), - - #[error(transparent)] - #[diagnostic(code(simulator::decode_error))] - Decode(#[from] DecodeError), -} - -impl QemuConnection { - pub async fn send_packet(&mut self, packet: KernelBoundPacket) -> Result<(), ConnectionError> { - let encoded = bincode::encode_to_vec(packet, bincode::config::standard())?; - let mut bytes = Vec::new(); - - bytes.extend((encoded.len() as u32).to_le_bytes()); - bytes.extend(encoded); - - self.stdin.write_all(&bytes).await?; - - Ok(()) - } - - pub async fn recv_packet(&mut self) -> Result { - let packet_size = self.stdout.read_u32_le().await.unwrap() as usize; - let mut buf = vec![0; packet_size]; - self.stdout.read_exact(&mut buf).await.unwrap(); - - Ok(bincode::decode_from_slice(&buf, bincode::config::standard()) - .unwrap() - .0) - } -} diff --git a/packages/host/src/devices/distance_sensor.rs b/packages/host/src/devices/distance_sensor.rs index 13f407a..a17148e 100644 --- a/packages/host/src/devices/distance_sensor.rs +++ b/packages/host/src/devices/distance_sensor.rs @@ -33,7 +33,7 @@ impl DistanceSensor { task: tokio::task::spawn(async move { loop { port.send( - SmartPortData::DistanceSensor(data.lock().await.clone()), + SmartPortData::DistanceSensor(*data.lock().await), start.elapsed().as_millis() as u32, ) .await; diff --git a/packages/host/src/lib.rs b/packages/host/src/lib.rs index 1156f36..f2c6366 100644 --- a/packages/host/src/lib.rs +++ b/packages/host/src/lib.rs @@ -3,5 +3,3 @@ pub mod devices; pub mod peripherals; pub use vex_v5_qemu_protocol as protocol; - -pub(crate) mod connection; diff --git a/packages/host/src/peripherals/display.rs b/packages/host/src/peripherals/display.rs index d80412c..68767cc 100644 --- a/packages/host/src/peripherals/display.rs +++ b/packages/host/src/peripherals/display.rs @@ -1,115 +1,126 @@ -use std::{ - sync::{Arc, Mutex}, - time::Instant, -}; +use std::{pin::Pin, sync::Mutex, time::{Duration, Instant}}; -use image::{ImageBuffer, Rgb, RgbImage}; use tokio::{ sync::{ mpsc::{Receiver, Sender}, watch, }, - task::AbortHandle, + task::AbortHandle, time::{sleep, Sleep}, }; -// use vex_v5_display_simulator::{ -// DisplayRenderer, Pack, TextLine, TextOptions, DEFAULT_BACKGROUND, DEFAULT_FOREGROUND, -// }; +use vex_v5_display_simulator::{ColorTheme, DisplayRenderer, Pixmap, TextOptions}; use vex_v5_qemu_protocol::{ - display::{DrawCommand, TextLocation}, + display::DrawCommand, DisplayCommand, KernelBoundPacket, }; #[derive(Debug)] pub struct Display { task: AbortHandle, - data_rx: watch::Receiver>>, + data_rx: watch::Receiver>>, } impl Display { + pub const WIDTH: u32 = 480; + pub const HEIGHT: u32 = 272; + pub fn new(_tx: Sender, mut rx: Receiver) -> Self { let (data_tx, data_rx) = watch::channel(Mutex::new(None)); Self { task: tokio::spawn(async move { - // let mut renderer = DisplayRenderer::new(DEFAULT_FOREGROUND, DEFAULT_BACKGROUND); + let start = Instant::now(); + let mut renderer = DisplayRenderer::new(ColorTheme::Dark); + renderer.draw_header("User".to_string(), start.elapsed()); + let mut interval = tokio::time::interval(Duration::from_secs(1)); + + loop { + tokio::select! { + _ = interval.tick() => { + renderer.draw_header("User".to_string(), start.elapsed()); - // while let Some(command) = rx.recv().await { - // let mut new_frame = None; - // match command { - // DisplayCommand::Draw { - // command, - // color, - // clip_region: _, - // } => { - // renderer.foreground_color = Pack::unpack(color.0); - // match command { - // DrawCommand::Fill(shape) => { - // renderer.draw(shape, false); - // } - // DrawCommand::Stroke(shape) => { - // renderer.draw(shape, true); - // } - // DrawCommand::Text { - // data, - // size, - // location, - // opaque, - // background, - // } => { - // renderer.background_color = Pack::unpack(background.0); - // let coords = match location { - // TextLocation::Coordinates(coords) => coords, - // TextLocation::Line(line) => TextLine(line).coords(), - // }; + if let Some(frame) = renderer.render(false) { + _ = data_tx.send(Mutex::new(Some(frame))); + } + } + command = rx.recv() => { + if let Some(command) = command { + let mut new_frame = None; + match command { + DisplayCommand::Draw { + command, + color, + clip_region: _, + } => { + renderer.context.foreground_color = color; + match command { + DrawCommand::Fill(shape) => { + renderer.draw(shape, false); + } + DrawCommand::Stroke(shape) => { + renderer.draw(shape, true); + } + DrawCommand::Text { + data, + size, + position, + opaque, + background, + } => { + renderer.context.background_color = background; - // renderer.write_text( - // data, - // coords, - // !opaque, - // TextOptions { size: size.into() }, - // ); - // } - // DrawCommand::CopyBuffer { - // top_left, - // bottom_right, - // stride, - // buffer, - // } => { - // let buffer = bytemuck::cast_slice(&buffer); - // renderer.draw_buffer( - // buffer, - // top_left, - // bottom_right, - // stride.get().into(), - // ); - // } - // } - // } - // DisplayCommand::Erase { - // color, - // clip_region: _, - // } => { - // renderer.foreground_color = Pack::unpack(color.0); - // renderer.erase(); - // } - // DisplayCommand::Render => { - // new_frame = renderer.render(true); - // } - // DisplayCommand::DisableDoubleBuffering => { - // renderer.disable_double_buffer(); - // } - // DisplayCommand::Scroll { .. } => { - // todo!() - // } - // } + renderer.draw_text( + data, + position, + !opaque, + TextOptions { size: size.into(), ..Default::default() }, + ); + } + DrawCommand::CopyBuffer { + top_left, + bottom_right, + stride, + buffer, + } => { + let buffer = bytemuck::cast_slice(&buffer); + renderer.draw_buffer( + buffer, + top_left, + bottom_right, + stride.get().into(), + ); + } + } + } + DisplayCommand::Erase { + color, + clip_region: _, + } => { + renderer.context.foreground_color = color; + renderer.erase(); + } + DisplayCommand::Render => { + new_frame = renderer.render(true); + } + DisplayCommand::DisableDoubleBuffering => { + renderer.disable_double_buffer(); + } + DisplayCommand::Scroll { .. } => { + todo!() + } + } - // if new_frame.is_none() { - // new_frame = renderer.render(false); - // } + if new_frame.is_none() { + new_frame = renderer.render(false); + } - // if let Some(frame) = new_frame { - // _ = data_tx.send(Mutex::new(Some(frame))); - // } - // } + if let Some(frame) = new_frame { + _ = data_tx.send(Mutex::new(Some(frame))); + } + } else { + break; + } + } + } + } }) .abort_handle(), data_rx, @@ -122,7 +133,7 @@ impl Display { /// /// If this function is called too slowly, it will skip to the most recent /// frame. - pub async fn next_frame(&mut self) -> Option { + pub async fn next_frame(&mut self) -> Option { self.data_rx.changed().await.ok()?; let frame = self.data_rx.borrow_and_update(); let mut frame = frame.lock().unwrap(); diff --git a/packages/host/src/peripherals/mod.rs b/packages/host/src/peripherals/mod.rs index a9e476d..d80bd39 100644 --- a/packages/host/src/peripherals/mod.rs +++ b/packages/host/src/peripherals/mod.rs @@ -2,10 +2,12 @@ pub mod battery; pub mod display; pub mod smartport; pub mod usb; +pub mod touch; use battery::Battery; use display::Display; use smartport::SmartPort; +use touch::Touchscreen; use usb::Usb; #[derive(Debug)] @@ -36,5 +38,6 @@ pub struct Peripherals { pub port_21: SmartPort, pub display: Display, + pub touch: Touchscreen, // TODO: onboard ADI, controllers, display/touch, usb, sdcard } diff --git a/packages/host/src/peripherals/touch.rs b/packages/host/src/peripherals/touch.rs new file mode 100644 index 0000000..1f3d0c6 --- /dev/null +++ b/packages/host/src/peripherals/touch.rs @@ -0,0 +1,64 @@ +use std::{sync::Arc, time::Duration}; + +use tokio::{ + sync::{mpsc::Sender, Mutex, Notify}, + task::AbortHandle, + time::sleep, +}; +use vex_v5_qemu_protocol::{ + geometry::Point2, + touch::{TouchData, TouchEvent}, + KernelBoundPacket, +}; + +#[derive(Debug)] +pub struct Touchscreen { + data: Arc>, + notify: Arc, + task: AbortHandle, +} + +impl Touchscreen { + pub const UPDATE_INTERVAL: Duration = Duration::from_millis(5); + + pub fn new(tx: Sender) -> Self { + let data = Arc::new(Mutex::new(TouchData::default())); + let notify = Arc::new(Notify::new()); + + Self { + data: data.clone(), + notify: notify.clone(), + task: tokio::task::spawn(async move { + loop { + notify.notified().await; + + let data = *data.lock().await; + tx.send(KernelBoundPacket::Touch(data)).await.unwrap(); + + sleep(Self::UPDATE_INTERVAL).await; + } + }).abort_handle(), + } + } + + pub async fn set_data(&mut self, data: TouchData) { + *self.data.lock().await = data; + self.notify.notify_one(); + } + + pub async fn set_point(&mut self, point: Point2) { + self.data.lock().await.point = point; + self.notify.notify_one(); + } + + pub async fn set_event(&mut self, event: TouchEvent) { + self.data.lock().await.event = event; + self.notify.notify_one(); + } +} + +impl Drop for Touchscreen { + fn drop(&mut self) { + self.task.abort(); + } +} diff --git a/packages/kernel/src/protocol.rs b/packages/kernel/src/protocol.rs index f3d51b1..37857c1 100644 --- a/packages/kernel/src/protocol.rs +++ b/packages/kernel/src/protocol.rs @@ -2,7 +2,7 @@ use alloc::{vec, vec::Vec}; use bincode::error::{DecodeError, EncodeError}; use embedded_io::{Read as EIORead, Write as EIOWrite, ReadExactError}; -use semihosting::io::{stdout, ErrorKind, Write}; +use semihosting::io::{stdout, Write}; use snafu::Snafu; use vex_v5_qemu_protocol::{HostBoundPacket, KernelBoundPacket}; @@ -14,25 +14,15 @@ pub enum ProtocolError { Encode { inner: EncodeError }, } -fn semihosting_write_with_retry(bytes: &[u8]) -> Result<(), semihosting::io::Error> { - match stdout().unwrap().write_all(bytes) { - Err(err) if err.kind() == ErrorKind::Other && err.raw_os_error() == Some(0) => { - semihosting_write_with_retry(bytes) // no idea what the fuck is going on here but sure whatever man - } - res => res, - } -} - pub fn send_packet(packet: HostBoundPacket) -> Result<(), ProtocolError> { let encoded = bincode::encode_to_vec(packet, bincode::config::standard()) .map_err(|err| ProtocolError::Encode { inner: err })?; let mut bytes = Vec::new(); - bytes.extend((encoded.len() as u32).to_le_bytes()); bytes.extend(encoded); - semihosting_write_with_retry(&bytes).unwrap(); + stdout().unwrap().write_all(&bytes).unwrap(); Ok(()) } diff --git a/packages/kernel/src/sdk/display.rs b/packages/kernel/src/sdk/display.rs index 89412ff..c9fbe7f 100644 --- a/packages/kernel/src/sdk/display.rs +++ b/packages/kernel/src/sdk/display.rs @@ -12,7 +12,7 @@ use core::{ use vex_sdk::*; use vex_v5_qemu_protocol::{ - display::{Color, DrawCommand, ScrollLocation, Shape, TextLocation, TextSize}, + display::{Color, DrawCommand, ScrollLocation, Shape, TextSize}, geometry::{Point2, Rect}, DisplayCommand, HostBoundPacket, }; @@ -166,8 +166,9 @@ impl Display { &mut self, data: String, size: TextSize, - location: TextLocation, + position: Point2, opaque: bool, + foreground: Color, background: Color, ) -> Result<(), ProtocolError> { protocol::send_packet(HostBoundPacket::DisplayCommand { @@ -175,34 +176,56 @@ impl Display { command: DrawCommand::Text { data, size, - location, + position, opaque, background, }, - color: self.foreground, + color: foreground, clip_region: self.clip_region, }, }) } + + #[allow(unused)] + pub fn draw_text_with_foreground( + &mut self, + data: String, + size: TextSize, + position: Point2, + opaque: bool, + ) -> Result<(), ProtocolError> { + self.draw_text( + data, + size, + position, + opaque, + self.foreground, + self.background, + ) + } } pub fn draw_error_box(message: [Option<&str>; 3]) { let mut display = DISPLAY.lock(); - display.fill( - Shape::Rectangle { - top_left: Point2 { x: 50, y: 50 }, - bottom_right: Point2 { x: 340, y: 140 }, - }, - Color(0x8b0000), - ).unwrap(); + display + .fill( + Shape::Rectangle { + top_left: Point2 { x: 50, y: 50 }, + bottom_right: Point2 { x: 340, y: 120 }, + }, + Color(0x8b0000), + ) + .unwrap(); - display.stroke( - Shape::Rectangle { - top_left: Point2 { x: 50, y: 50 }, - bottom_right: Point2 { x: 340, y: 140 }, - }, - Color(0xffffff), - ).unwrap(); + display + .stroke( + Shape::Rectangle { + top_left: Point2 { x: 50, y: 50 }, + bottom_right: Point2 { x: 340, y: 120 }, + }, + Color(0xffffff), + ) + .unwrap(); for (n, line) in message.iter().enumerate() { if let Some(text) = line { @@ -212,11 +235,12 @@ pub fn draw_error_box(message: [Option<&str>; 3]) { .draw_text( text.to_string(), TextSize::Small, - TextLocation::Coordinates(Point2 { + Point2 { x: 80, y: 70 + 20 * (n as i32), - }), + }, false, + Color(0xffffff), Color(0x8b0000), ) .unwrap() @@ -309,7 +333,7 @@ pub extern "C" fn vexDisplayLineDraw(x1: i32, y1: i32, x2: i32, y2: i32) { DISPLAY .lock() // it's a line so we don't stroke - .fill_with_foreground(Shape::Line { + .stroke_with_foreground(Shape::Line { start: Point2 { x: x1, y: y1 }, end: Point2 { x: x2, y: y2 }, }) @@ -318,7 +342,7 @@ pub extern "C" fn vexDisplayLineDraw(x1: i32, y1: i32, x2: i32, y2: i32) { pub extern "C" fn vexDisplayLineClear(x1: i32, y1: i32, x2: i32, y2: i32) { DISPLAY .lock() - .fill_with_background(Shape::Line { + .stroke_with_background(Shape::Line { start: Point2 { x: x1, y: y1 }, end: Point2 { x: x2, y: y2 }, }) @@ -352,15 +376,20 @@ pub extern "C" fn vexDisplayRectFill(x1: i32, y1: i32, x2: i32, y2: i32) { .unwrap() } pub extern "C" fn vexDisplayCircleDraw(xc: i32, yc: i32, radius: i32) { - DISPLAY - .lock() - .stroke_with_foreground(Shape::Circle { - center: Point2 { x: xc, y: yc }, - radius: radius as _, - }) - .unwrap() + if radius > 0 { + DISPLAY + .lock() + .stroke_with_foreground(Shape::Circle { + center: Point2 { x: xc, y: yc }, + radius: radius as _, + }) + .unwrap() + + } } pub extern "C" fn vexDisplayCircleClear(xc: i32, yc: i32, radius: i32) { + if radius > 0 { + DISPLAY .lock() .fill_with_background(Shape::Circle { @@ -368,15 +397,18 @@ pub extern "C" fn vexDisplayCircleClear(xc: i32, yc: i32, radius: i32) { radius: radius as _, }) .unwrap() + } } pub extern "C" fn vexDisplayCircleFill(xc: i32, yc: i32, radius: i32) { - DISPLAY - .lock() - .fill_with_foreground(Shape::Circle { - center: Point2 { x: xc, y: yc }, - radius: radius as _, - }) - .unwrap() + if radius > 0 { + DISPLAY + .lock() + .fill_with_foreground(Shape::Circle { + center: Point2 { x: xc, y: yc }, + radius: radius as _, + }) + .unwrap() + } } pub extern "C" fn vexDisplayTextSize(n: u32, d: u32) {} pub extern "C" fn vexDisplayFontNamedSet(pFontName: *const c_char) {} @@ -441,52 +473,127 @@ pub extern "C" fn vexImagePngRead( ) -> u32 { Default::default() } -pub extern "C" fn vexDisplayVPrintf( +pub unsafe extern "C" fn vexDisplayVPrintf( xpos: i32, ypos: i32, bOpaque: i32, format: *const c_char, - args: VaList<'_, '_>, + mut args: VaList<'_, '_>, ) { + let mut data = String::new(); + unsafe { + _ = printf_compat::format( + format, + args.as_va_list(), + printf_compat::output::fmt_write(&mut data), + ); + } + DISPLAY + .lock() + .draw_text_with_foreground( + data, + TextSize::Normal, + Point2 { x: xpos, y: ypos }, + bOpaque == 1, + ) + .unwrap(); } -pub extern "C" fn vexDisplayVString(nLineNumber: i32, format: *const c_char, args: VaList<'_, '_>) { +pub unsafe extern "C" fn vexDisplayVString( + nLineNumber: i32, + format: *const c_char, + args: VaList<'_, '_>, +) { + unsafe { + display_string_impl( + TextSize::Normal, + Point2 { + x: 0, + y: nLineNumber * 20 + 34, + }, + format, + args, + ); + } } -pub extern "C" fn vexDisplayVStringAt( +pub unsafe extern "C" fn vexDisplayVStringAt( xpos: i32, ypos: i32, format: *const c_char, args: VaList<'_, '_>, ) { + unsafe { + display_string_impl(TextSize::Normal, Point2 { x: xpos, y: ypos }, format, args); + } } -pub extern "C" fn vexDisplayVBigString( +pub unsafe extern "C" fn vexDisplayVBigString( nLineNumber: i32, format: *const c_char, args: VaList<'_, '_>, ) { -} -pub extern "C" fn vexDisplayVBigStringAt( + todo!("measure line height for non-normal text sizes"); + // unsafe { + // display_string_impl( + // TextSize::Large, + // Point2 { + // x: 0, + // y: nLineNumber * ?? + 34, + // }, + // format, + // args, + // ); + // } +} +pub unsafe extern "C" fn vexDisplayVBigStringAt( xpos: i32, ypos: i32, format: *const c_char, args: VaList<'_, '_>, ) { + unsafe { + display_string_impl(TextSize::Large, Point2 { x: xpos, y: ypos }, format, args); + } } -pub extern "C" fn vexDisplayVSmallStringAt( +pub unsafe extern "C" fn vexDisplayVSmallStringAt( xpos: i32, ypos: i32, format: *const c_char, args: VaList<'_, '_>, ) { + unsafe { + display_string_impl(TextSize::Small, Point2 { x: xpos, y: ypos }, format, args); + } } -pub extern "C" fn vexDisplayVCenteredString( +pub unsafe extern "C" fn vexDisplayVCenteredString( nLineNumber: i32, format: *const c_char, args: VaList<'_, '_>, ) { + todo!(); } -pub extern "C" fn vexDisplayVBigCenteredString( +pub unsafe extern "C" fn vexDisplayVBigCenteredString( nLineNumber: i32, format: *const c_char, args: VaList<'_, '_>, ) { + todo!("measure line height for non-normal text sizes"); +} + +unsafe fn display_string_impl( + size: TextSize, + point: Point2, + format: *const c_char, + mut args: VaList<'_, '_>, +) { + let mut data = String::new(); + unsafe { + _ = printf_compat::format( + format, + args.as_va_list(), + printf_compat::output::fmt_write(&mut data), + ); + } + DISPLAY + .lock() + .draw_text_with_foreground(data, size, point, false) + .unwrap(); } diff --git a/packages/kernel/src/sdk/task.rs b/packages/kernel/src/sdk/task.rs index a02b5eb..6568646 100644 --- a/packages/kernel/src/sdk/task.rs +++ b/packages/kernel/src/sdk/task.rs @@ -6,7 +6,7 @@ use embedded_io::Write; use vex_v5_qemu_protocol::KernelBoundPacket; use super::{BATTERY, SMARTPORTS}; -use crate::{peripherals::UART1, protocol::recv_packet, sdk::USB1}; +use crate::{peripherals::UART1, protocol::recv_packet, sdk::{TOUCH, USB1}}; /// Adds a new simple task to the task scheduler. pub extern "C" fn vexTaskAdd( @@ -67,7 +67,10 @@ pub extern "C" fn vexTasksRun() { id, data, timestamp, - } => {} + } => {}, + KernelBoundPacket::Touch(data) => { + TOUCH.lock().data = data; + } _ => panic!("Unexpected kernel-bound packet {:?}", packet), } } diff --git a/packages/kernel/src/sdk/touch.rs b/packages/kernel/src/sdk/touch.rs index f0cd8a2..ce7177a 100644 --- a/packages/kernel/src/sdk/touch.rs +++ b/packages/kernel/src/sdk/touch.rs @@ -1,7 +1,44 @@ //! Brain Screen Touchscreen +use crate::sync::Mutex; use vex_sdk::*; +use vex_v5_qemu_protocol::{geometry::Point2, touch::{TouchData, TouchEvent}}; + +pub static TOUCH: Mutex = Mutex::new(Touchscreen::new()); + +pub struct Touchscreen { + pub data: TouchData, +} + +impl Touchscreen { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + Self { + data: TouchData { + event: TouchEvent::Release, + point: Point2 { x: 0, y: 0 }, + }, + } + } +} pub extern "C" fn vexTouchUserCallbackSet(callback: unsafe extern "C" fn(V5_TouchEvent, i32, i32)) { } -pub extern "C" fn vexTouchDataGet(status: *mut V5_TouchStatus) {} +/// # Safety +/// +/// - `device` must be a valid, non-null pointer to a V5_TouchStatus instance +pub unsafe extern "C" fn vexTouchDataGet(status: *mut V5_TouchStatus) { + let data = TOUCH.lock().data; + unsafe { + *status = V5_TouchStatus { + lastEvent: match data.event { + TouchEvent::Press => V5_TouchEvent::kTouchEventPress, + TouchEvent::Release => V5_TouchEvent::kTouchEventRelease, + }, + lastXpos: data.point.x, + lastYpos: data.point.y, + pressCount: 0, // TODO + releaseCount: 0, + } + } +} diff --git a/packages/protocol/Cargo.toml b/packages/protocol/Cargo.toml index 4354589..9272819 100644 --- a/packages/protocol/Cargo.toml +++ b/packages/protocol/Cargo.toml @@ -18,7 +18,7 @@ workspace = true [dependencies] vex-sdk = "0.19.0" -bincode = { version = "2.0.0-rc.3", default-features = false, features = [ +bincode = { version = "2.0.1", default-features = false, features = [ "derive", "alloc", ] } diff --git a/packages/protocol/src/display.rs b/packages/protocol/src/display.rs index 6948b00..8d10d5e 100644 --- a/packages/protocol/src/display.rs +++ b/packages/protocol/src/display.rs @@ -26,24 +26,12 @@ pub enum DrawCommand { Text { data: String, size: TextSize, - location: TextLocation, + position: Point2, opaque: bool, background: Color, }, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Encode, Decode, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum TextLocation { - Coordinates(Point2), - Line(i32), -} - -impl Default for TextLocation { - fn default() -> Self { - Self::Coordinates(Point2 { x: 0, y: 0 }) - } -} #[derive(Debug, Clone, PartialEq, Eq, Hash, Encode, Decode, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/packages/protocol/src/distance_sensor.rs b/packages/protocol/src/distance_sensor.rs index 4ae6bdf..6647192 100644 --- a/packages/protocol/src/distance_sensor.rs +++ b/packages/protocol/src/distance_sensor.rs @@ -2,14 +2,14 @@ use bincode::{Decode, Encode}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, PartialOrd, Encode, Decode)] +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Encode, Decode)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct DistanceSensorData { pub object: Option, pub status: u32, } -#[derive(Debug, Clone, PartialEq, PartialOrd, Encode, Decode)] +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Encode, Decode)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct DistanceObject { pub distance: u32, diff --git a/packages/protocol/src/lib.rs b/packages/protocol/src/lib.rs index 3ac14db..fc5c39c 100644 --- a/packages/protocol/src/lib.rs +++ b/packages/protocol/src/lib.rs @@ -15,6 +15,8 @@ use geometry::Rect; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use crate::touch::TouchData; + pub mod battery; pub mod code_signature; pub mod controller; @@ -22,6 +24,7 @@ pub mod display; pub mod distance_sensor; pub mod geometry; pub mod motor; +pub mod touch; /// A message sent from the guest to the host. #[derive(Debug, Clone, PartialEq, PartialOrd, Encode, Decode)] @@ -77,6 +80,7 @@ pub enum KernelBoundPacket { data: BatteryData, timestamp: u32, }, + Touch(TouchData), } #[derive(Debug, Clone, PartialEq, PartialOrd, Encode, Decode)] @@ -85,7 +89,7 @@ pub enum SmartPortData { DistanceSensor(DistanceSensorData), } -#[derive(Debug, Clone, PartialEq, PartialOrd, Encode, Decode)] +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Encode, Decode)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum SmartPortCommand {} diff --git a/packages/protocol/src/motor.rs b/packages/protocol/src/motor.rs index cf5682d..79929e6 100644 --- a/packages/protocol/src/motor.rs +++ b/packages/protocol/src/motor.rs @@ -1,11 +1,8 @@ -use bincode::{Decode, Encode}; use bitflags::bitflags; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::impl_bincode_bitflags; - -#[derive(Debug, Clone, PartialEq, PartialOrd, Encode, Decode)] +#[derive(Debug, Clone, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct MotorData { pub velocity: f64, @@ -36,7 +33,6 @@ bitflags! { const DRIVER_OVER_CURRENT = 0x08; } } -impl_bincode_bitflags!(MotorFaults); bitflags! { /// The status bits returned by a [`Motor`]. @@ -53,4 +49,3 @@ bitflags! { const ZERO_POSITION = 0x04; } } -impl_bincode_bitflags!(MotorFlags); diff --git a/packages/protocol/src/touch.rs b/packages/protocol/src/touch.rs new file mode 100644 index 0000000..4240bde --- /dev/null +++ b/packages/protocol/src/touch.rs @@ -0,0 +1,27 @@ +use bincode::{Decode, Encode}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use crate::geometry::Point2; + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Encode, Decode)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct TouchData { + pub point: Point2, + pub event: TouchEvent, +} + +impl Default for TouchData { + fn default() -> Self { + TouchData { + event: TouchEvent::Release, + point: Point2 { x: 0, y: 0 }, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Encode, Decode)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum TouchEvent { + Press, + Release, +}