From af4cd47b0cd8493117254a71a45210d67979989d Mon Sep 17 00:00:00 2001 From: cartermp Date: Fri, 27 Mar 2026 16:47:51 -0700 Subject: [PATCH] gpu --- Cargo.lock | 875 +++++++++++++++++++++------- Cargo.toml | 4 +- src/config.rs | 8 - src/main.rs | 120 +++- src/renderer.rs | 1466 ++++++++++++++++++++++++++++++----------------- 5 files changed, 1688 insertions(+), 785 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11134b2..79c5406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,7 +68,7 @@ dependencies = [ "log", "ndk", "ndk-context", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "num_enum", "thiserror 1.0.69", ] @@ -79,6 +79,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -103,6 +112,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "ash" +version = "0.37.3+1.3.251" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +dependencies = [ + "libloading 0.7.4", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -124,15 +142,30 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -151,13 +184,19 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2 0.5.2", + "objc2", ] [[package]] @@ -183,7 +222,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -242,12 +281,59 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "com" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "combine" version = "4.6.7" @@ -322,18 +408,23 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "ctor-lite" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e162d0c2e2068eb736b71e5597eff0b9944e6b973cd9f37b6a288ab9bf20e300" - [[package]] name = "cursor-icon" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "d3d12" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" +dependencies = [ + "bitflags 2.11.0", + "libloading 0.8.9", + "winapi", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -341,22 +432,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] -name = "dispatch2" -version = "0.3.1" +name = "dlib" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "bitflags 2.11.0", - "objc2 0.6.4", + "libloading 0.8.9", ] [[package]] -name = "dlib" -version = "0.5.3" +name = "document-features" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ - "libloading", + "litrs", ] [[package]] @@ -371,46 +461,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -[[package]] -name = "drm" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" -dependencies = [ - "bitflags 2.11.0", - "bytemuck", - "drm-ffi", - "drm-fourcc", - "libc", - "rustix 0.38.44", -] - -[[package]] -name = "drm-ffi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" -dependencies = [ - "drm-sys", - "rustix 1.1.4", -] - -[[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.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" -dependencies = [ - "libc", - "linux-raw-sys 0.9.4", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -433,17 +483,11 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" dependencies = [ - "bit-set", + "bit-set 0.8.0", "regex-automata", "regex-syntax", ] -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "filedescriptor" version = "0.8.3" @@ -477,6 +521,12 @@ 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.8.0" @@ -505,7 +555,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -560,6 +610,90 @@ dependencies = [ "wasip2", ] +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.11.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "winapi", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.11.0", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -570,18 +704,48 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.11.0", + "com", + "libc", + "libloading 0.8.9", + "thiserror 1.0.69", + "widestring", + "winapi", +] + [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "indexmap" version = "2.13.0" @@ -642,7 +806,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -665,6 +829,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading 0.8.9", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "lazy_static" version = "1.5.0" @@ -677,6 +858,16 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libloading" version = "0.8.9" @@ -707,15 +898,24 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "litrs" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] [[package]] name = "log" @@ -723,6 +923,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.8.0" @@ -747,6 +956,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metal" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb" +dependencies = [ + "bitflags 2.11.0", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -757,6 +981,27 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "naga" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" +dependencies = [ + "arrayvec", + "bit-set 0.5.3", + "bitflags 2.11.0", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "num-traits", + "rustc-hash", + "spirv", + "termcolor", + "thiserror 1.0.69", + "unicode-xid", +] + [[package]] name = "ndk" version = "0.9.0" @@ -766,7 +1011,7 @@ dependencies = [ "bitflags 2.11.0", "jni-sys 0.3.1", "log", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", "thiserror 1.0.69", @@ -778,6 +1023,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -801,6 +1055,15 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_enum" version = "0.7.6" @@ -820,7 +1083,16 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", ] [[package]] @@ -839,15 +1111,6 @@ dependencies = [ "objc2-encode", ] -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - [[package]] name = "objc2-app-kit" version = "0.2.2" @@ -857,11 +1120,11 @@ dependencies = [ "bitflags 2.11.0", "block2", "libc", - "objc2 0.5.2", + "objc2", "objc2-core-data", "objc2-core-image", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "objc2-foundation", + "objc2-quartz-core", ] [[package]] @@ -872,9 +1135,9 @@ checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.11.0", "block2", - "objc2 0.5.2", + "objc2", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -884,8 +1147,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-foundation", ] [[package]] @@ -896,32 +1159,8 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.11.0", "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.11.0", - "dispatch2", - "objc2 0.6.4", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.11.0", - "dispatch2", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-io-surface", + "objc2", + "objc2-foundation", ] [[package]] @@ -931,8 +1170,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-foundation", "objc2-metal", ] @@ -943,9 +1182,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ "block2", - "objc2 0.5.2", + "objc2", "objc2-contacts", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -964,29 +1203,7 @@ dependencies = [ "block2", "dispatch", "libc", - "objc2 0.5.2", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.11.0", - "objc2 0.6.4", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.11.0", - "objc2 0.6.4", - "objc2-core-foundation", + "objc2", ] [[package]] @@ -996,9 +1213,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2", - "objc2 0.5.2", + "objc2", "objc2-app-kit", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -1009,8 +1226,8 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.11.0", "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-foundation", ] [[package]] @@ -1021,31 +1238,19 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.11.0", "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-foundation", "objc2-metal", ] -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.11.0", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-foundation 0.3.2", -] - [[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", + "objc2", + "objc2-foundation", ] [[package]] @@ -1056,14 +1261,14 @@ checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.11.0", "block2", - "objc2 0.5.2", + "objc2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-link-presentation", - "objc2-quartz-core 0.2.2", + "objc2-quartz-core", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", @@ -1076,8 +1281,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-foundation", ] [[package]] @@ -1088,9 +1293,9 @@ checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.11.0", "block2", - "objc2 0.5.2", + "objc2", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -1118,6 +1323,35 @@ dependencies = [ "ttf-parser 0.25.1", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1141,7 +1375,7 @@ checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1182,6 +1416,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "portable-pty" version = "0.8.1" @@ -1203,6 +1443,12 @@ dependencies = [ "winreg", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1221,6 +1467,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" + [[package]] name = "quick-xml" version = "0.39.2" @@ -1245,6 +1497,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -1295,6 +1553,18 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.38.44" @@ -1342,6 +1612,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sctk-adwaita" version = "0.10.1" @@ -1381,7 +1657,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1460,6 +1736,15 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -1501,43 +1786,37 @@ dependencies = [ ] [[package]] -name = "softbuffer" -version = "0.4.8" +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "as-raw-xcb-connection", - "bytemuck", - "drm", - "fastrand", - "js-sys", - "memmap2", - "ndk", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", - "raw-window-handle", - "redox_syscall 0.5.18", - "rustix 1.1.4", - "tiny-xlib", - "tracing", - "wasm-bindgen", - "wayland-backend", - "wayland-client", - "wayland-sys", - "web-sys", - "windows-sys 0.61.2", - "x11rb", + "bitflags 2.11.0", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -1571,15 +1850,26 @@ dependencies = [ name = "term" version = "0.1.0" dependencies = [ + "bytemuck", "fontdue", - "objc2 0.5.2", + "objc2", + "pollster", "portable-pty", - "softbuffer", "syntect", "vte", + "wgpu", "winit", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "termios" version = "0.2.2" @@ -1615,7 +1905,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1626,7 +1916,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1654,19 +1944,6 @@ dependencies = [ "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", - "pkg-config", - "tracing", -] - [[package]] name = "toml_datetime" version = "1.0.1+spec-1.1.0" @@ -1737,6 +2014,18 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1835,7 +2124,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -1977,6 +2266,121 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wgpu" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e37c7b9921b75dfd26dd973fdcbce36f13dfa6e2dc82aece584e0ed48c355c" +dependencies = [ + "arrayvec", + "cfg-if", + "cfg_aliases 0.1.1", + "document-features", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50819ab545b867d8a454d1d756b90cd5f15da1f2943334ca314af10583c9d39" +dependencies = [ + "arrayvec", + "bit-vec 0.6.3", + "bitflags 2.11.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash", + "smallvec", + "thiserror 1.0.69", + "web-sys", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172e490a87295564f3fcc0f165798d87386f6231b04d4548bca458cbbfd63222" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.5.3", + "bitflags 2.11.0", + "block", + "cfg_aliases 0.1.1", + "core-graphics-types", + "d3d12", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.9", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash", + "smallvec", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winapi", +] + +[[package]] +name = "wgpu-types" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef" +dependencies = [ + "bitflags 2.11.0", + "js-sys", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -2008,6 +2412,25 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -2184,7 +2607,7 @@ dependencies = [ "block2", "bytemuck", "calloop", - "cfg_aliases", + "cfg_aliases 0.2.1", "concurrent-queue", "core-foundation", "core-graphics", @@ -2194,9 +2617,9 @@ dependencies = [ "libc", "memmap2", "ndk", - "objc2 0.5.2", + "objc2", "objc2-app-kit", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-ui-kit", "orbclient", "percent-encoding", @@ -2267,7 +2690,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading", + "libloading 0.8.9", "once_cell", "rustix 1.1.4", "x11rb-protocol", @@ -2304,6 +2727,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + [[package]] name = "zerocopy" version = "0.8.47" @@ -2321,5 +2750,5 @@ checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] diff --git a/Cargo.toml b/Cargo.toml index 40f103b..eda5683 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,10 +19,12 @@ path = "src/bin/tdiff.rs" [dependencies] winit = "0.30" +wgpu = "0.20" +pollster = "0.3" +bytemuck = { version = "1", features = ["derive"] } [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.5" -softbuffer = "0.4" vte = "0.13" portable-pty = "0.8" fontdue = "0.8" diff --git a/src/config.rs b/src/config.rs index f3b108e..80acbeb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,14 +14,6 @@ impl Color { ((self.r as u32) << 16) | ((self.g as u32) << 8) | (self.b as u32) } - /// Pack as 0xFF_RR_GG_BB — fully opaque pixel for the softbuffer framebuffer. - /// On macOS the framebuffer format is BGRA; the top byte is the alpha channel. - /// Using 0xFF makes the pixel opaque; 0x00 (from `to_u32`) makes it transparent, - /// letting the NSVisualEffectView vibrancy show through. - pub fn to_u32_opaque(self) -> u32 { - 0xFF_00_00_00 | self.to_u32() - } - pub fn blend(self, fg: Color, alpha: u8) -> Color { let a = alpha as u32; let ia = 255 - a; diff --git a/src/main.rs b/src/main.rs index 451b8c2..1db0038 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ mod renderer; mod terminal; use std::io::{Read, Write}; -use std::num::NonZeroU32; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -192,8 +191,8 @@ fn find_urls( struct App { // Window / rendering window: Option>, - context: Option>>, - surface: Option, Arc>>, + wgpu_surface: Option>, + wgpu_config: Option, renderer: Option, // Tabs @@ -227,8 +226,8 @@ impl App { fn new(first_tab: Tab, proxy: EventLoopProxy) -> Self { Self { window: None, - context: None, - surface: None, + wgpu_surface: None, + wgpu_config: None, renderer: None, tabs: vec![first_tab], active_tab: 0, @@ -846,7 +845,7 @@ impl App { } }); - let surface = match &mut self.surface { + let wgpu_surface = match &self.wgpu_surface { Some(s) => s, None => return, }; @@ -855,19 +854,28 @@ impl App { None => return, }; - surface - .resize( - NonZeroU32::new(w as u32).unwrap(), - NonZeroU32::new(h as u32).unwrap(), - ) - .unwrap(); + let output = match wgpu_surface.get_current_texture() { + Ok(o) => o, + Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => { + if let Some(cfg) = &self.wgpu_config { + wgpu_surface.configure(&renderer.device, cfg); + } + return; + } + Err(e) => { + eprintln!("surface error: {e}"); + return; + } + }; + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); let state = unsafe { &*state_ptr }; - let mut buf = surface.buffer_mut().unwrap(); renderer.render( - buf.as_mut(), - w, - h, + &view, + w as u32, + h as u32, state, show_cur, ghost_owned.as_deref(), @@ -879,10 +887,21 @@ impl App { &url_underlines, ); - buf.present().unwrap(); + output.present(); } fn on_resize(&mut self, width: u32, height: u32) { + // Reconfigure the wgpu swap chain + if let (Some(surface), Some(cfg), Some(renderer)) = ( + &self.wgpu_surface, + &mut self.wgpu_config, + &self.renderer, + ) { + cfg.width = width.max(1); + cfg.height = height.max(1); + surface.configure(&renderer.device, cfg); + } + let (Some(renderer), _) = (&self.renderer, ()) else { return; }; @@ -913,9 +932,62 @@ impl ApplicationHandler for App { .with_inner_size(winit::dpi::LogicalSize::new(WINDOW_WIDTH, WINDOW_HEIGHT)); let window = Arc::new(event_loop.create_window(attrs).unwrap()); - let scale = window.scale_factor(); - let renderer = Renderer::new(scale); + // ── wgpu initialisation ─────────────────────────────────────────────── + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + ..Default::default() + }); + + // Surface must be created before adapter so we can pass compatible_surface. + // Arc satisfies the 'static bound required for Surface<'static>. + let surface: wgpu::Surface<'static> = instance + .create_surface(window.clone()) + .expect("create wgpu surface"); + + let adapter = pollster::block_on(instance.request_adapter( + &wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }, + )) + .expect("no suitable GPU adapter"); + + let (device, queue) = pollster::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + label: Some("term"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + }, + None, + )) + .expect("request_device"); + let size = window.inner_size(); + let caps = surface.get_capabilities(&adapter); + // Prefer non-sRGB to match existing colour values (raw u8 → float passthrough) + let surface_format = caps + .formats + .iter() + .copied() + .find(|f| !f.is_srgb()) + .unwrap_or(caps.formats[0]); + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + width: size.width.max(1), + height: size.height.max(1), + present_mode: wgpu::PresentMode::AutoVsync, + alpha_mode: caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &config); + + // ── Build renderer ──────────────────────────────────────────────────── + let scale = window.scale_factor(); + let renderer = Renderer::new(device, queue, surface_format, scale); + let (cols, rows) = { let cw = renderer.cell_width; let ch = renderer.cell_height; @@ -936,10 +1008,8 @@ impl ApplicationHandler for App { let _ = tab.pty_master.resize(pty_size); } - let context = softbuffer::Context::new(window.clone()).unwrap(); - let surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); - self.context = Some(context); - self.surface = Some(surface); + self.wgpu_surface = Some(surface); + self.wgpu_config = Some(config); self.renderer = Some(renderer); self.window = Some(window); } @@ -963,7 +1033,9 @@ impl ApplicationHandler for App { } WindowEvent::ScaleFactorChanged { scale_factor, .. } => { - self.renderer = Some(Renderer::new(scale_factor)); + if let Some(r) = self.renderer.take() { + self.renderer = Some(r.rescale(scale_factor)); + } } WindowEvent::ModifiersChanged(state) => { diff --git a/src/renderer.rs b/src/renderer.rs index c8a1f9e..05cbc95 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,33 +1,13 @@ use crate::config::*; use crate::terminal::TerminalState; +use bytemuck::{Pod, Zeroable}; use std::collections::HashMap; -// ── Internal glyph cache ────────────────────────────────────────────────────── - -struct Glyph { - width: usize, - height: usize, - xmin: i32, - ymin: i32, - data: Vec, -} - -pub struct Renderer { - font: fontdue::Font, - cache: HashMap, - pub cell_width: usize, - pub cell_height: usize, - pub baseline: i32, - pub tab_bar_height: usize, - font_size: f32, -} +// ── tcat gutter detection ───────────────────────────────────────────────────── /// Find the first content column of a tcat gutter line, or 0 if this row is -/// not tcat output. tcat renders: ` {digits} │ content` — we detect the -/// BOX-DRAWINGS LIGHT VERTICAL (U+2502) with a space on each side, preceded -/// only by spaces and digits. +/// not tcat output. fn tcat_gutter_end(state: &TerminalState, row: usize, vis_cols: usize) -> usize { - // │ can't appear at col 0 (needs a space before it) or at the last col for c in 1..vis_cols.saturating_sub(1) { if state.visual_cell(row, c).c != '\u{2502}' { continue; @@ -38,30 +18,250 @@ fn tcat_gutter_end(state: &TerminalState, row: usize, vis_cols: usize) -> usize if state.visual_cell(row, c + 1).c != ' ' { continue; } - // Everything in 0..c-1 must be spaces/null/digits, with at least one digit let mut prefix = 0..c - 1; let has_digit = prefix.clone().any(|i| state.visual_cell(row, i).c.is_ascii_digit()); - let all_ok = prefix.all(|i| { - matches!(state.visual_cell(row, i).c, ' ' | '\0' | '0'..='9') - }); + let all_ok = prefix + .all(|i| matches!(state.visual_cell(row, i).c, ' ' | '\0' | '0'..='9')); if has_digit && all_ok { - return c + 2; // column after "│ " + return c + 2; } } 0 } +// ── Shader sources ──────────────────────────────────────────────────────────── + +const RECT_SHADER: &str = r#" +struct Uni { res: vec4 } +@group(0) @binding(0) var uni: Uni; + +struct Inst { + @location(0) pos: vec2, + @location(1) sz: vec2, + @location(2) color: vec4, +} +struct Out { + @builtin(position) clip: vec4, + @location(0) col: vec4, +} + +@vertex fn vs(@builtin(vertex_index) vi: u32, i: Inst) -> Out { + let uv = vec2(f32(vi & 1u), f32(vi >> 1u)); + let px = i.pos + uv * i.sz; + let ndc = px / uni.res.xy * vec2(2., -2.) + vec2(-1., 1.); + return Out(vec4(ndc, 0., 1.), i.color); +} +@fragment fn fs(o: Out) -> @location(0) vec4 { return o.col; } +"#; + +const GLYPH_SHADER: &str = r#" +struct Uni { res: vec4 } +@group(0) @binding(0) var uni: Uni; +@group(1) @binding(0) var atlas_t: texture_2d; +@group(1) @binding(1) var atlas_s: sampler; + +struct Inst { + @location(0) pos: vec2, + @location(1) sz: vec2, + @location(2) uv_pos: vec2, + @location(3) uv_sz: vec2, + @location(4) fg: vec4, +} +struct Out { + @builtin(position) clip: vec4, + @location(0) uv: vec2, + @location(1) fg: vec4, +} + +@vertex fn vs(@builtin(vertex_index) vi: u32, i: Inst) -> Out { + let uv = vec2(f32(vi & 1u), f32(vi >> 1u)); + let px = i.pos + uv * i.sz; + let ndc = px / uni.res.xy * vec2(2., -2.) + vec2(-1., 1.); + return Out(vec4(ndc, 0., 1.), i.uv_pos + uv * i.uv_sz, i.fg); +} +@fragment fn fs(o: Out) -> @location(0) vec4 { + let a = textureSample(atlas_t, atlas_s, o.uv).r; + return vec4(o.fg.rgb, o.fg.a * a); +} +"#; + +// ── Instance types ──────────────────────────────────────────────────────────── + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct RectInst { + pos: [f32; 2], + sz: [f32; 2], + color: [f32; 4], +} + +impl RectInst { + fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: 8, + shader_location: 1, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: 16, + shader_location: 2, + format: wgpu::VertexFormat::Float32x4, + }, + ], + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct GlyphInst { + pos: [f32; 2], + sz: [f32; 2], + uv_pos: [f32; 2], + uv_sz: [f32; 2], + fg: [f32; 4], +} + +impl GlyphInst { + fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: 8, + shader_location: 1, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: 16, + shader_location: 2, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: 24, + shader_location: 3, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: 32, + shader_location: 4, + format: wgpu::VertexFormat::Float32x4, + }, + ], + } + } +} + +// ── Atlas ───────────────────────────────────────────────────────────────────── + +const ATLAS_SIZE: u32 = 1024; + +#[derive(Clone)] +struct AtlasEntry { + uv_x: f32, + uv_y: f32, + uv_w: f32, + uv_h: f32, + // fontdue placement offsets (pixels from cell origin) + glyph_x: i32, + glyph_y: i32, + w: u32, + h: u32, +} + +// ── Color helpers ───────────────────────────────────────────────────────────── + +fn c2f(c: Color) -> [f32; 4] { + [c.r as f32 / 255., c.g as f32 / 255., c.b as f32 / 255., 1.] +} + +fn c2fa(c: Color, a: f32) -> [f32; 4] { + [c.r as f32 / 255., c.g as f32 / 255., c.b as f32 / 255., a] +} + +fn rgb_f(r: u8, g: u8, b: u8) -> [f32; 4] { + [r as f32 / 255., g as f32 / 255., b as f32 / 255., 1.] +} + +// ── Instance list helpers ───────────────────────────────────────────────────── + +fn push_rect(v: &mut Vec, x: f32, y: f32, w: f32, h: f32, color: [f32; 4]) { + if w > 0. && h > 0. { + v.push(RectInst { pos: [x, y], sz: [w, h], color }); + } +} + +// ── Renderer ────────────────────────────────────────────────────────────────── + +pub struct Renderer { + pub device: wgpu::Device, + queue: wgpu::Queue, + + rect_pipeline: wgpu::RenderPipeline, + glyph_pipeline: wgpu::RenderPipeline, + + uni_buf: wgpu::Buffer, + uni_bg: wgpu::BindGroup, + + atlas_tex: wgpu::Texture, + _atlas_view: wgpu::TextureView, + _atlas_sampler: wgpu::Sampler, + atlas_bg: wgpu::BindGroup, + _atlas_bgl: wgpu::BindGroupLayout, + atlas_cache: HashMap>, + atlas_x: u32, + atlas_y: u32, + atlas_row_h: u32, + + // Reusable GPU instance buffers (grown on demand) + rect_buf: wgpu::Buffer, + rect_buf_cap: usize, + glyph_buf: wgpu::Buffer, + glyph_buf_cap: usize, + + font: fontdue::Font, + pub cell_width: usize, + pub cell_height: usize, + pub baseline: i32, + pub tab_bar_height: usize, + font_size: f32, + surface_format: wgpu::TextureFormat, +} + impl Renderer { - pub fn new(scale_factor: f64) -> Self { + pub fn new( + device: wgpu::Device, + queue: wgpu::Queue, + surface_format: wgpu::TextureFormat, + scale_factor: f64, + ) -> Self { + // ── Font ────────────────────────────────────────────────────────────── let font_size = (FONT_SIZE_PT * scale_factor as f32).round(); - let font_data = Self::load_font(); - let settings = fontdue::FontSettings { - scale: font_size, - collection_index: 0, - load_substitutions: true, - }; - let font = - fontdue::Font::from_bytes(font_data.as_slice(), settings).expect("failed to load font"); + let font_data = include_bytes!("../assets/JetBrainsMono-Regular.ttf").to_vec(); + let font = fontdue::Font::from_bytes( + font_data.as_slice(), + fontdue::FontSettings { + scale: font_size, + collection_index: 0, + load_substitutions: true, + }, + ) + .expect("font load"); let (m, _) = font.rasterize('M', font_size); let cell_width = m.advance_width.ceil() as usize; @@ -69,237 +269,449 @@ impl Renderer { let ascent = lm.ascent.ceil() as i32; let descent = (-lm.descent).ceil() as i32; let gap = lm.line_gap.ceil() as i32; - let cell_height = (ascent + descent + gap).max(font_size as i32 + 4) as usize; + let cell_height = + (ascent + descent + gap).max(font_size as i32 + 4) as usize; let tab_bar_height = cell_height + 16; + // ── Uniform buffer (resolution) ─────────────────────────────────────── + let uni_buf = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("uni"), + size: 16, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let uni_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("uni_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let uni_bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("uni_bg"), + layout: &uni_bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uni_buf.as_entire_binding(), + }], + }); + + // ── Atlas texture ───────────────────────────────────────────────────── + let atlas_tex = device.create_texture(&wgpu::TextureDescriptor { + label: Some("atlas"), + size: wgpu::Extent3d { + width: ATLAS_SIZE, + height: ATLAS_SIZE, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::R8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + let atlas_view = + atlas_tex.create_view(&wgpu::TextureViewDescriptor::default()); + let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("atlas_smp"), + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + let atlas_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("atlas_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::Filtering, + ), + count: None, + }, + ], + }); + let atlas_bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("atlas_bg"), + layout: &atlas_bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&atlas_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&atlas_sampler), + }, + ], + }); + + // ── Pipelines ───────────────────────────────────────────────────────── + let rect_pipeline = Self::make_rect_pipeline( + &device, + surface_format, + &uni_bgl, + ); + let glyph_pipeline = Self::make_glyph_pipeline( + &device, + surface_format, + &uni_bgl, + &atlas_bgl, + ); + + // ── Instance buffers (pre-allocate for typical terminal) ─────────────── + let init_rect = 8192usize; + let init_glyph = 8192usize; + let rect_buf = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("rect_buf"), + size: (init_rect * std::mem::size_of::()) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let glyph_buf = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("glyph_buf"), + size: (init_glyph * std::mem::size_of::()) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + Self { + device, + queue, + rect_pipeline, + glyph_pipeline, + uni_buf, + uni_bg, + atlas_tex, + _atlas_view: atlas_view, + _atlas_sampler: atlas_sampler, + atlas_bg, + _atlas_bgl: atlas_bgl, + atlas_cache: HashMap::new(), + atlas_x: 0, + atlas_y: 0, + atlas_row_h: 0, + rect_buf, + rect_buf_cap: init_rect, + glyph_buf, + glyph_buf_cap: init_glyph, font, - cache: HashMap::new(), cell_width, cell_height, baseline: ascent, tab_bar_height, font_size, + surface_format, } } - fn load_font() -> Vec { - // Embedded JetBrains Mono — always available, excellent Unicode coverage. - include_bytes!("../assets/JetBrainsMono-Regular.ttf").to_vec() + /// Rebuild the renderer for a new DPI scale, reusing the existing GPU device/queue. + pub fn rescale(self, scale_factor: f64) -> Self { + let fmt = self.surface_format; + Self::new(self.device, self.queue, fmt, scale_factor) } - // ── Low-level blit ──────────────────────────────────────────────────────── - - fn ensure(&mut self, c: char) { - if !self.cache.contains_key(&c) { - let (m, bm) = self.font.rasterize(c, self.font_size); - self.cache.insert( - c, - Glyph { - width: m.width, - height: m.height, - xmin: m.xmin, - ymin: m.ymin, - data: bm, - }, - ); - } + // ── Pipeline constructors ───────────────────────────────────────────────── + + fn make_rect_pipeline( + device: &wgpu::Device, + fmt: wgpu::TextureFormat, + uni_bgl: &wgpu::BindGroupLayout, + ) -> wgpu::RenderPipeline { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("rect_shader"), + source: wgpu::ShaderSource::Wgsl(RECT_SHADER.into()), + }); + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[uni_bgl], + push_constant_ranges: &[], + }); + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("rect_pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs", + buffers: &[RectInst::desc()], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs", + targets: &[Some(wgpu::ColorTargetState { + format: fmt, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }) } - fn blit( - &mut self, - buf: &mut [u32], - bw: usize, - bh: usize, - px: usize, - py: usize, - c: char, - fg: Color, - ) { - // Skip characters not in the font — avoids rendering the .notdef box. + fn make_glyph_pipeline( + device: &wgpu::Device, + fmt: wgpu::TextureFormat, + uni_bgl: &wgpu::BindGroupLayout, + atlas_bgl: &wgpu::BindGroupLayout, + ) -> wgpu::RenderPipeline { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("glyph_shader"), + source: wgpu::ShaderSource::Wgsl(GLYPH_SHADER.into()), + }); + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[uni_bgl, atlas_bgl], + push_constant_ranges: &[], + }); + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("glyph_pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs", + buffers: &[GlyphInst::desc()], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs", + targets: &[Some(wgpu::ColorTargetState { + format: fmt, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }) + } + + // ── Atlas management ────────────────────────────────────────────────────── + + /// Ensure glyph `c` is in the atlas. Returns a clone of the entry, or None + /// if the character has no visible glyph (space, control, .notdef). + fn ensure_glyph(&mut self, c: char) -> Option { + if let Some(entry) = self.atlas_cache.get(&c) { + return entry.clone(); + } + // Skip chars not in font if self.font.lookup_glyph_index(c) == 0 { - return; + self.atlas_cache.insert(c, None); + return None; } - self.ensure(c); - let g = &self.cache[&c]; - if g.width == 0 || g.height == 0 { - return; + let (m, bitmap) = self.font.rasterize(c, self.font_size); + if m.width == 0 || m.height == 0 { + self.atlas_cache.insert(c, None); + return None; } - let gx0 = px as i32 + g.xmin; - let gy0 = py as i32 + self.baseline - (g.ymin + g.height as i32); - for gy in 0..g.height { - let sy = gy0 + gy as i32; - if sy < 0 || sy >= bh as i32 { - continue; - } - let sy = sy as usize; - for gx in 0..g.width { - let alpha = g.data[gy * g.width + gx]; - if alpha == 0 { - continue; - } - let sx = gx0 + gx as i32; - if sx < 0 || sx >= bw as i32 { - continue; - } - let sx = sx as usize; - let idx = sy * bw + sx; - let ex = buf[idx]; - let bg = Color { - r: ((ex >> 16) & 0xff) as u8, - g: ((ex >> 8) & 0xff) as u8, - b: (ex & 0xff) as u8, - }; - buf[idx] = bg.blend(fg, alpha).to_u32_opaque(); - } + let gw = m.width as u32; + let gh = m.height as u32; + + // Advance to next row if needed (1px padding) + if self.atlas_x + gw + 1 > ATLAS_SIZE { + self.atlas_y += self.atlas_row_h + 1; + self.atlas_x = 0; + self.atlas_row_h = 0; + } + if self.atlas_y + gh > ATLAS_SIZE { + // Atlas full — evict and restart (rare; terminals have small glyph sets) + self.atlas_cache.clear(); + self.atlas_x = 0; + self.atlas_y = 0; + self.atlas_row_h = 0; } + + let ax = self.atlas_x; + let ay = self.atlas_y; + + self.queue.write_texture( + wgpu::ImageCopyTexture { + texture: &self.atlas_tex, + mip_level: 0, + origin: wgpu::Origin3d { x: ax, y: ay, z: 0 }, + aspect: wgpu::TextureAspect::All, + }, + &bitmap, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(gw), + rows_per_image: Some(gh), + }, + wgpu::Extent3d { width: gw, height: gh, depth_or_array_layers: 1 }, + ); + + self.atlas_x += gw + 1; + self.atlas_row_h = self.atlas_row_h.max(gh); + + // Compute pixel offset to place glyph at the correct position relative + // to the cell origin (top-left of the cell). + // baseline is measured from cell top. fontdue ymin is from baseline up. + let glyph_y = self.baseline - (m.ymin + m.height as i32); + let glyph_x = m.xmin; + + let entry = AtlasEntry { + uv_x: ax as f32 / ATLAS_SIZE as f32, + uv_y: ay as f32 / ATLAS_SIZE as f32, + uv_w: gw as f32 / ATLAS_SIZE as f32, + uv_h: gh as f32 / ATLAS_SIZE as f32, + glyph_x, + glyph_y, + w: gw, + h: gh, + }; + self.atlas_cache.insert(c, Some(entry.clone())); + Some(entry) } - fn fill_rect( - buf: &mut [u32], - bw: usize, - bh: usize, - px: usize, - py: usize, - rw: usize, - rh: usize, - color: u32, + // ── Glyph emit helper ───────────────────────────────────────────────────── + + fn emit_char( + &mut self, + glyphs: &mut Vec, + c: char, + px: f32, + py: f32, + fg: [f32; 4], ) { - for y in py..(py + rh).min(bh) { - let base = y * bw; - for x in px..(px + rw).min(bw) { - buf[base + x] = color; - } + if let Some(e) = self.ensure_glyph(c) { + glyphs.push(GlyphInst { + pos: [px + e.glyph_x as f32, py + e.glyph_y as f32], + sz: [e.w as f32, e.h as f32], + uv_pos: [e.uv_x, e.uv_y], + uv_sz: [e.uv_w, e.uv_h], + fg, + }); } } - /// Draw a Unicode Block Element (U+2580–U+259F) as a direct pixel fill. - /// Returns true if the character was handled; false means fall through to fontdue. - fn draw_block_char( - buf: &mut [u32], - bw: usize, - bh: usize, - px: usize, // cell left pixel - py: usize, // cell top pixel - cw: usize, - ch: usize, + // ── Block character rects ───────────────────────────────────────────────── + + /// Decompose a Unicode block/Braille char into colored fill rects. + /// Returns false if `c` is not a handled block character. + fn push_block_char( + block_rects: &mut Vec, + px: f32, + py: f32, + cw: f32, + ch: f32, c: char, - fg: u32, + fg: [f32; 4], ) -> bool { - // Fill [x0..x1) × [y0..y1) within the cell with fg, clipped. - fn fill( - buf: &mut [u32], - bw: usize, - bh: usize, - px: usize, - py: usize, - cw: usize, - ch: usize, - x0: usize, - y0: usize, - x1: usize, - y1: usize, - fg: u32, - ) { - let x1 = x1.min(cw); - let y1 = y1.min(ch); - if x0 >= x1 || y0 >= y1 { - return; - } - for dy in y0..y1 { - let y = py + dy; - if y >= bh { - break; - } - let row = y * bw; - for dx in x0..x1 { - let x = px + dx; - if x < bw { - buf[row + x] = fg; - } - } - } - } + // Fill [x0..x1) × [y0..y1) within the cell (in cell-relative pixels). macro_rules! f { - ($x0:expr,$y0:expr,$x1:expr,$y1:expr) => { - fill(buf, bw, bh, px, py, cw, ch, $x0, $y0, $x1, $y1, fg) - }; + ($x0:expr, $y0:expr, $x1:expr, $y1:expr) => {{ + let x0 = $x0 as f32; + let y0 = $y0 as f32; + let x1 = ($x1 as f32).min(cw); + let y1 = ($y1 as f32).min(ch); + if x1 > x0 && y1 > y0 { + block_rects.push(RectInst { + pos: [px + x0, py + y0], + sz: [x1 - x0, y1 - y0], + color: fg, + }); + } + }}; } + let cwu = cw as usize; + let chu = ch as usize; match c { - // ── Vertical (lower N/8) ────────────────────────────────────────── - '\u{2581}' => f!(0, ch * 7 / 8, cw, ch), - '\u{2582}' => f!(0, ch * 3 / 4, cw, ch), - '\u{2583}' => f!(0, ch * 5 / 8, cw, ch), - '\u{2584}' => f!(0, ch / 2, cw, ch), - '\u{2585}' => f!(0, ch * 3 / 8, cw, ch), - '\u{2586}' => f!(0, ch / 4, cw, ch), - '\u{2587}' => f!(0, ch / 8, cw, ch), - '\u{2588}' => f!(0, 0, cw, ch), - // ── Upper half / upper 1/8 ─────────────────────────────────────── - '\u{2580}' => f!(0, 0, cw, ch / 2), - '\u{2594}' => f!(0, 0, cw, (ch / 8).max(1)), - // ── Horizontal (left N/8) ───────────────────────────────────────── - '\u{258F}' => f!(0, 0, (cw / 8).max(1), ch), - '\u{258E}' => f!(0, 0, cw / 4, ch), - '\u{258D}' => f!(0, 0, cw * 3 / 8, ch), - '\u{258C}' => f!(0, 0, cw / 2, ch), - '\u{258B}' => f!(0, 0, cw * 5 / 8, ch), - '\u{258A}' => f!(0, 0, cw * 3 / 4, ch), - '\u{2589}' => f!(0, 0, cw * 7 / 8, ch), - // ── Right half / right 1/8 ─────────────────────────────────────── - '\u{2590}' => f!(cw / 2, 0, cw, ch), - '\u{2595}' => f!(cw * 7 / 8, 0, cw, ch), - // ── Quadrants ──────────────────────────────────────────────────── - '\u{2596}' => f!(0, ch / 2, cw / 2, ch), - '\u{2597}' => f!(cw / 2, ch / 2, cw, ch), - '\u{2598}' => f!(0, 0, cw / 2, ch / 2), - '\u{259D}' => f!(cw / 2, 0, cw, ch / 2), + '\u{2581}' => f!(0, chu * 7 / 8, cwu, chu), + '\u{2582}' => f!(0, chu * 3 / 4, cwu, chu), + '\u{2583}' => f!(0, chu * 5 / 8, cwu, chu), + '\u{2584}' => f!(0, chu / 2, cwu, chu), + '\u{2585}' => f!(0, chu * 3 / 8, cwu, chu), + '\u{2586}' => f!(0, chu / 4, cwu, chu), + '\u{2587}' => f!(0, chu / 8, cwu, chu), + '\u{2588}' => f!(0, 0, cwu, chu), + '\u{2580}' => f!(0, 0, cwu, chu / 2), + '\u{2594}' => f!(0, 0, cwu, (chu / 8).max(1)), + '\u{258F}' => f!(0, 0, (cwu / 8).max(1), chu), + '\u{258E}' => f!(0, 0, cwu / 4, chu), + '\u{258D}' => f!(0, 0, cwu * 3 / 8, chu), + '\u{258C}' => f!(0, 0, cwu / 2, chu), + '\u{258B}' => f!(0, 0, cwu * 5 / 8, chu), + '\u{258A}' => f!(0, 0, cwu * 3 / 4, chu), + '\u{2589}' => f!(0, 0, cwu * 7 / 8, chu), + '\u{2590}' => f!(cw / 2., 0., cwu, chu), + '\u{2595}' => f!(cwu * 7 / 8, 0, cwu, chu), + '\u{2596}' => f!(0, chu / 2, cwu / 2, chu), + '\u{2597}' => f!(cwu / 2, chu / 2, cwu, chu), + '\u{2598}' => f!(0, 0, cwu / 2, chu / 2), + '\u{259D}' => f!(cwu / 2, 0, cwu, chu / 2), '\u{2599}' => { - f!(0, 0, cw / 2, ch / 2); - f!(0, ch / 2, cw, ch); + f!(0, 0, cwu / 2, chu / 2); + f!(0, chu / 2, cwu, chu); } '\u{259A}' => { - f!(0, 0, cw / 2, ch / 2); - f!(cw / 2, ch / 2, cw, ch); + f!(0, 0, cwu / 2, chu / 2); + f!(cwu / 2, chu / 2, cwu, chu); } '\u{259B}' => { - f!(0, 0, cw, ch / 2); - f!(0, ch / 2, cw / 2, ch); + f!(0, 0, cwu, chu / 2); + f!(0, chu / 2, cwu / 2, chu); } '\u{259C}' => { - f!(0, 0, cw, ch / 2); - f!(cw / 2, ch / 2, cw, ch); + f!(0, 0, cwu, chu / 2); + f!(cwu / 2, chu / 2, cwu, chu); } '\u{259E}' => { - f!(cw / 2, 0, cw, ch / 2); - f!(0, ch / 2, cw / 2, ch); + f!(cwu / 2, 0, cwu, chu / 2); + f!(0, chu / 2, cwu / 2, chu); } '\u{259F}' => { - f!(cw / 2, 0, cw, ch / 2); - f!(0, ch / 2, cw, ch); + f!(cwu / 2, 0, cwu, chu / 2); + f!(0, chu / 2, cwu, chu); } - // ── Braille patterns U+2800–U+28FF ─────────────────────────────── - // Each character encodes an 8-dot (2×4) grid. - // Bit layout: 0=r0c0, 1=r1c0, 2=r2c0, 3=r0c1, - // 4=r1c1, 5=r2c1, 6=r3c0, 7=r3c1 '\u{2800}'..='\u{28FF}' => { let bits = c as u32 - 0x2800; if bits == 0 { - return true; // blank braille — nothing to draw + return true; } - // dot column widths / row heights (split cell evenly into 2×4) - let col0_w = cw / 2; - let col1_w = cw - col0_w; - let row_h = [ch / 4, ch / 4, ch / 4, ch - 3 * (ch / 4)]; + let col0_w = cwu / 2; + let col1_w = cwu - col0_w; + let row_h = [chu / 4, chu / 4, chu / 4, chu - 3 * (chu / 4)]; let mut ry = 0usize; for row in 0..4usize { let rh = row_h[row]; - // col 0: bits 0,1,2,6 let bit_c0 = [0u32, 1, 2, 6][row]; if bits & (1 << bit_c0) != 0 { f!(0, ry, col0_w, ry + rh); } - // col 1: bits 3,4,5,7 let bit_c1 = [3u32, 4, 5, 7][row]; if bits & (1 << bit_c1) != 0 { f!(col0_w, ry, col0_w + col1_w, ry + rh); @@ -312,117 +724,74 @@ impl Renderer { true } - /// Thin circle outline (1–2 px) centred at (cx, cy) with radius r. - fn stroke_circle( - buf: &mut [u32], - bw: usize, - bh: usize, - cx: usize, - cy: usize, - r: usize, - color: u32, - ) { - let ro = (r as i32) + 1; - let ri = (r as i32) - 1; - for dy in -ro..=ro { - for dx in -ro..=ro { - let d2 = dx * dx + dy * dy; - if d2 <= ro * ro && d2 > ri * ri { - let px = cx as i32 + dx; - let py = cy as i32 + dy; - if px >= 0 && px < bw as i32 && py >= 0 && py < bh as i32 { - buf[py as usize * bw + px as usize] = color; - } - } - } + // ── Buffer resize helper ────────────────────────────────────────────────── + + fn ensure_rect_buf(&mut self, need: usize) { + if need > self.rect_buf_cap { + let cap = need.next_power_of_two().max(8192); + self.rect_buf = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("rect_buf"), + size: (cap * std::mem::size_of::()) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + self.rect_buf_cap = cap; } } - /// Filled rounded rectangle with radius `r` pixels (circular corners). - fn fill_rounded( - buf: &mut [u32], - bw: usize, - bh: usize, - x: usize, - y: usize, - w: usize, - h: usize, - r: usize, - color: u32, - ) { - let r2 = (r * r) as i32; - for dy in 0..h { - for dx in 0..w { - let in_x_corner = dx < r || dx + r >= w; - let in_y_corner = dy < r || dy + r >= h; - if in_x_corner && in_y_corner { - let cx = if dx < r { - (r - dx) as i32 - } else { - (dx + r + 1 - w) as i32 - }; - let cy = if dy < r { - (r - dy) as i32 - } else { - (dy + r + 1 - h) as i32 - }; - if cx * cx + cy * cy > r2 { - continue; - } - } - let px = x + dx; - let py = y + dy; - if px < bw && py < bh { - buf[py * bw + px] = color; - } - } + fn ensure_glyph_buf(&mut self, need: usize) { + if need > self.glyph_buf_cap { + let cap = need.next_power_of_two().max(8192); + self.glyph_buf = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("glyph_buf"), + size: (cap * std::mem::size_of::()) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + self.glyph_buf_cap = cap; } } - // ── Tab bar ─────────────────────────────────────────────────────────────── + // ── Tab bar builder ─────────────────────────────────────────────────────── - /// `hover` — visual index of hovered tab, or `tabs.len()` for the + button. - /// `drag` — `(from_orig, to_visual, cursor_x)` while a tab is being dragged. - fn draw_tab_bar( + fn build_tab_bar( &mut self, - buf: &mut [u32], - bw: usize, - bh: usize, + bg_rects: &mut Vec, + fg_rects: &mut Vec, + glyphs: &mut Vec, + bw: f32, tabs: &[String], active: usize, hover: Option, drag: Option<(usize, usize, f64)>, ) { - let tby = self.tab_bar_height; - let cw = self.cell_width; - let ch = self.cell_height; - - // Neutral dark palette — mirrors terminal background aesthetic - let bar_bg = Color::new(0x14, 0x14, 0x14).to_u32_opaque(); // slightly darker than bg - let outline_col = Color::new(0x58, 0x58, 0x58).to_u32_opaque(); // medium gray — active outline - let hover_bg = Color::new(0x26, 0x26, 0x26).to_u32_opaque(); // subtle hover fill - let sep_col = Color::new(0x2e, 0x2e, 0x2e).to_u32_opaque(); // dim separator - let bottom_col = Color::new(0x2e, 0x2e, 0x2e).to_u32_opaque(); // bottom rule - let fg_active = DEFAULT_FG; - let fg_inactive = Color::new(0x66, 0x66, 0x66); // dim gray - let fg_shortcut = Color::new(0x3a, 0x3a, 0x3a); // very dim - - // Bar fill + bottom border - Self::fill_rect(buf, bw, bh, 0, 0, bw, tby, bar_bg); - Self::fill_rect(buf, bw, bh, 0, tby.saturating_sub(1), bw, 1, bottom_col); - - // Reserve right side for + button + let tby = self.tab_bar_height as f32; + let cw = self.cell_width as f32; + let ch = self.cell_height as f32; + + let bar_bg = rgb_f(0x14, 0x14, 0x14); + let outline = rgb_f(0x58, 0x58, 0x58); + let hover_bg = rgb_f(0x26, 0x26, 0x26); + let sep_col = rgb_f(0x2e, 0x2e, 0x2e); + let bottom = rgb_f(0x2e, 0x2e, 0x2e); + let fg_act = c2f(DEFAULT_FG); + let fg_inact = rgb_f(0x66, 0x66, 0x66); + let fg_sc = rgb_f(0x3a, 0x3a, 0x3a); + + // Bar background + bottom border + push_rect(bg_rects, 0., 0., bw, tby, bar_bg); + push_rect(bg_rects, 0., tby - 1., bw, 1., bottom); + let plus_area = tby; - let tabs_w = bw.saturating_sub(plus_area); + let tabs_w = bw - plus_area; let n = tabs.len().max(1); - let tab_w = tabs_w / n; - let pad_v = 4usize; - let pill_h = tby.saturating_sub(pad_v * 2); - let text_y = (tby.saturating_sub(ch)) / 2; - let shortcut_w = 3 * cw; // ⌘ + digit + 1 pad - - // Compute visual tab order for drag preview. - // visual_order[vi] = original tab index shown at visual position vi. + let tab_w = tabs_w / n as f32; + let pad_v = 4.; + let pill_h = tby - pad_v * 2.; + let text_y = (tby - ch) / 2.; + let shortcut_w = 3. * cw; + + // Compute visual order for drag let visual_order: Vec = if let Some((from, to, _)) = drag { let mut order: Vec = (0..tabs.len()).collect(); if from < order.len() { @@ -443,178 +812,124 @@ impl Renderer { let title = &tabs[orig_idx]; let is_active = orig_idx == active; let is_dragging = drag_orig == Some(orig_idx); - // Suppress hover highlight while dragging let is_hover = drag.is_none() && hover == Some(vi) && !is_active; - let tx = vi * tab_w; + let tx = vi as f32 * tab_w; if tx >= tabs_w { break; } - let tw = if vi + 1 == n { - tabs_w.saturating_sub(tx) - } else { - tab_w - }; - let pill_x = tx + 4; - let pill_w = tw.saturating_sub(8); + let tw = if vi + 1 == n { tabs_w - tx } else { tab_w }; + let pill_x = tx + 4.; + let pill_w = tw - 8.; - // ── Dragging: draw a dim placeholder "drop here" slot ───────────── if is_dragging { - // Separator rules + // Separator on dragged slot if vi > 0 && vi != visual_active && vi != visual_active + 1 { - let sep_top = pad_v + 4; - let sep_bottom = tby.saturating_sub(pad_v + 4); - for y in sep_top..sep_bottom { - if tx < bw { - buf[y * bw + tx] = sep_col; - } - } + push_rect(fg_rects, tx, pad_v + 4., 1., tby - (pad_v + 4.) * 2., sep_col); } - // Dim dashed-ish outline as drop-target hint - let ghost_col = Color::new(0x38, 0x38, 0x38).to_u32_opaque(); - if pill_w > 2 && pill_h > 2 { - Self::fill_rounded(buf, bw, bh, pill_x, pad_v, pill_w, pill_h, 5, ghost_col); - Self::fill_rounded(buf, bw, bh, pill_x + 1, pad_v + 1, pill_w - 2, pill_h - 2, 4, bar_bg); + // Ghost outline (simple rect approximation for rounded rect) + let ghost = rgb_f(0x38, 0x38, 0x38); + if pill_w > 2. && pill_h > 2. { + push_rect(fg_rects, pill_x, pad_v, pill_w, pill_h, ghost); + push_rect(fg_rects, pill_x + 1., pad_v + 1., pill_w - 2., pill_h - 2., bar_bg); } continue; } - // ── Hover fill ──────────────────────────────────────────────────── if is_hover { - Self::fill_rounded(buf, bw, bh, pill_x, pad_v, pill_w, pill_h, 4, hover_bg); + push_rect(fg_rects, pill_x, pad_v, pill_w, pill_h, hover_bg); } - - // ── Active: outlined rounded rect ───────────────────────────────── - if is_active && pill_w > 2 && pill_h > 2 { - Self::fill_rounded(buf, bw, bh, pill_x, pad_v, pill_w, pill_h, 5, outline_col); - Self::fill_rounded( - buf, - bw, - bh, - pill_x + 1, - pad_v + 1, - pill_w - 2, - pill_h - 2, - 4, - bar_bg, - ); + if is_active && pill_w > 2. && pill_h > 2. { + push_rect(fg_rects, pill_x, pad_v, pill_w, pill_h, outline); + push_rect(fg_rects, pill_x + 1., pad_v + 1., pill_w - 2., pill_h - 2., bar_bg); } - // ── Separator (hide on both sides of the visual active position) ── + // Separator if vi > 0 && vi != visual_active && vi != visual_active + 1 { - let sep_top = pad_v + 4; - let sep_bottom = tby.saturating_sub(pad_v + 4); - for y in sep_top..sep_bottom { - if tx < bw { - buf[y * bw + tx] = sep_col; - } - } + push_rect(fg_rects, tx, pad_v + 4., 1., tby - (pad_v + 4.) * 2., sep_col); } - // ── ⌘N shortcut (original index so ⌘1 stays bound to tab[0]) ───── + // ⌘N shortcut let shortcut = format!("\u{2318}{}", orig_idx + 1); - let sc_x = tx + tw.saturating_sub(shortcut_w); + let sc_x = tx + tw - shortcut_w; let mut col_x = sc_x; for c in shortcut.chars() { - if col_x + cw > tx + tw { - break; - } - self.blit(buf, bw, bh, col_x, text_y, c, fg_shortcut); + if col_x + cw > tx + tw { break; } + self.emit_char(glyphs, c, col_x, text_y, fg_sc); col_x += cw; } - // ── Title — left-aligned, truncated ─────────────────────────────── - let fg = if is_active { fg_active } else { fg_inactive }; + // Title + let fg = if is_active { fg_act } else { fg_inact }; let left_pad = tx + cw; - let right_edge = tx + tw.saturating_sub(shortcut_w + cw); - let max_cols = right_edge.saturating_sub(left_pad) / cw; + let right_edge = tx + tw - shortcut_w - cw; + let max_cols = ((right_edge - left_pad) / cw).max(0.) as usize; let chars: Vec = title.chars().collect(); let show_n = chars.len().min(max_cols); let truncated = show_n < chars.len(); for (ci, &c) in chars[..show_n].iter().enumerate() { - let px = left_pad + ci * cw; - if truncated && ci + 1 == show_n { - self.blit(buf, bw, bh, px, text_y, '\u{2026}', fg); - } else { - self.blit(buf, bw, bh, px, text_y, c, fg); - } + let cpx = left_pad + ci as f32 * cw; + let draw_c = if truncated && ci + 1 == show_n { '\u{2026}' } else { c }; + self.emit_char(glyphs, draw_c, cpx, text_y, fg); } } - // ── Floating dragged tab (rendered on top, follows cursor) ──────────── + // Floating dragged tab if let Some((from_orig, _, cursor_x)) = drag { let title = &tabs[from_orig]; let is_active = from_orig == active; - // Center the floating tab under the cursor, clamped inside tab area - let half = (tab_w / 2) as isize; - let float_left = ((cursor_x as isize) - half) - .max(0) - .min(tabs_w.saturating_sub(tab_w) as isize) as usize; - let pill_x = float_left + 4; - let pill_w = tab_w.saturating_sub(8); - - // Bright lifted outline with a slightly lighter fill - let lifted_outline = Color::new(0xa0, 0xa0, 0xa0).to_u32_opaque(); - let lifted_bg = Color::new(0x20, 0x20, 0x20).to_u32_opaque(); - if pill_w > 2 && pill_h > 2 { - Self::fill_rounded(buf, bw, bh, pill_x, pad_v, pill_w, pill_h, 5, lifted_outline); - Self::fill_rounded(buf, bw, bh, pill_x + 1, pad_v + 1, pill_w - 2, pill_h - 2, 4, lifted_bg); + let half = tab_w / 2.; + let float_left = (cursor_x as f32 - half) + .max(0.) + .min(tabs_w - tab_w); + let pill_x = float_left + 4.; + let pill_w = tab_w - 8.; + + let lifted_outline = rgb_f(0xa0, 0xa0, 0xa0); + let lifted_bg = rgb_f(0x20, 0x20, 0x20); + if pill_w > 2. && pill_h > 2. { + push_rect(fg_rects, pill_x, pad_v, pill_w, pill_h, lifted_outline); + push_rect(fg_rects, pill_x + 1., pad_v + 1., pill_w - 2., pill_h - 2., lifted_bg); } - // Title (active color always — it's the "held" tab) - let fg = if is_active { fg_active } else { Color::new(0xcc, 0xcc, 0xcc) }; + let fg = if is_active { fg_act } else { rgb_f(0xcc, 0xcc, 0xcc) }; let left_pad = float_left + cw; - let right_edge = float_left + tab_w.saturating_sub(shortcut_w + cw); - let max_cols = right_edge.saturating_sub(left_pad) / cw; + let right_edge = float_left + tab_w - shortcut_w - cw; + let max_cols = ((right_edge - left_pad) / cw).max(0.) as usize; let chars: Vec = title.chars().collect(); let show_n = chars.len().min(max_cols); let truncated = show_n < chars.len(); for (ci, &c) in chars[..show_n].iter().enumerate() { - let px = left_pad + ci * cw; - if truncated && ci + 1 == show_n { - self.blit(buf, bw, bh, px, text_y, '\u{2026}', fg); - } else { - self.blit(buf, bw, bh, px, text_y, c, fg); - } + let cpx = left_pad + ci as f32 * cw; + let draw_c = if truncated && ci + 1 == show_n { '\u{2026}' } else { c }; + self.emit_char(glyphs, draw_c, cpx, text_y, fg); } } - // ── + button (circle outline + cross) ──────────────────────────────── + // + button: draw a simple cross (two thin rects) let plus_hover = hover == Some(tabs.len()); let plus_col = if plus_hover { - Color::new(0x88, 0x88, 0x88).to_u32_opaque() // hovered + rgb_f(0x88, 0x88, 0x88) } else { - Color::new(0x44, 0x44, 0x44).to_u32_opaque() // normal + rgb_f(0x44, 0x44, 0x44) }; - let plus_cx = bw.saturating_sub(plus_area / 2); - let plus_cy = tby / 2; - let plus_r = 9usize; - Self::stroke_circle(buf, bw, bh, plus_cx, plus_cy, plus_r, plus_col); - let arm = (plus_r / 2) as i32; - for d in -arm..=arm { - let px = (plus_cx as i32 + d) as usize; - let py = (plus_cy as i32 + d) as usize; - if px < bw && plus_cy < bh { - buf[plus_cy * bw + px] = plus_col; - } - if plus_cx < bw && py < bh { - buf[py * bw + plus_cx] = plus_col; - } - } + let plus_cx = bw - plus_area / 2.; + let plus_cy = tby / 2.; + let arm = 5.; + // horizontal bar + push_rect(fg_rects, plus_cx - arm, plus_cy - 1., arm * 2., 2., plus_col); + // vertical bar + push_rect(fg_rects, plus_cx - 1., plus_cy - arm, 2., arm * 2., plus_col); } // ── Public render ───────────────────────────────────────────────────────── - /// `hover` — visual index of hovered tab, or `tabs.len()` for the + button. - /// `selection` — normalized (r0, c0, r1, c1) in viewport coordinates, if any. - /// `drag` — (from_orig, to_visual, cursor_x) preview while dragging a tab. - /// `url_underlines` — (row, col_start, col_end) spans to underline (URLs, shown when Cmd held). pub fn render( &mut self, - buf: &mut [u32], - bw: usize, - bh: usize, + view: &wgpu::TextureView, + width: u32, + height: u32, state: &TerminalState, show_cursor: bool, ghost: Option<&str>, @@ -625,27 +940,40 @@ impl Renderer { drag: Option<(usize, usize, f64)>, url_underlines: &[(usize, usize, usize)], ) { - // Start fully transparent. Cells with DEFAULT_BG will be filled with - // BG_ALPHA opacity; everything else (tab bar, glyphs, cursor…) is - // fully opaque. On macOS this lets the NSVisualEffectView blur show - // through in background areas. - buf.fill(0); + let bw = width as f32; + let bh = height as f32; + let tby = self.tab_bar_height as f32; + let cw = self.cell_width as f32; + let ch = self.cell_height as f32; - // ── Tab bar ─────────────────────────────────────────────────────────── - self.draw_tab_bar(buf, bw, bh, tabs, active_tab, hover, drag); + // Update resolution uniform + let res: [f32; 4] = [bw, bh, 0., 0.]; + self.queue.write_buffer(&self.uni_buf, 0, bytemuck::cast_slice(&res)); - let tby = self.tab_bar_height; // y offset for terminal content - let cw = self.cell_width; - let ch = self.cell_height; - let term_h = bh.saturating_sub(tby); - let vis_rows = (term_h / ch).min(state.rows); - let vis_cols = (bw / cw).min(state.cols); + let mut bg_rects: Vec = Vec::with_capacity(4096); + let mut block_rects: Vec = Vec::new(); + let mut glyphs: Vec = Vec::with_capacity(4096); + let mut fg_rects: Vec = Vec::new(); - // ── 1. Terminal grid ────────────────────────────────────────────────── - let sel_bg = Color::new(0x26, 0x4a, 0x7a).to_u32_opaque(); // muted blue selection (opaque) + // ── Tab bar ─────────────────────────────────────────────────────────── + self.build_tab_bar( + &mut bg_rects, + &mut fg_rects, + &mut glyphs, + bw, + tabs, + active_tab, + hover, + drag, + ); + + // ── Terminal grid ───────────────────────────────────────────────────── + let term_h = bh - tby; + let vis_rows = ((term_h / ch) as usize).min(state.rows); + let vis_cols = ((bw / cw) as usize).min(state.cols); + let sel_bg = c2fa(Color::new(0x26, 0x4a, 0x7a), 1.0); for row in 0..vis_rows { - // Per-row: detect tcat gutter so it's excluded from selection highlight. let gutter = if selection.is_some() { tcat_gutter_end(state, row, vis_cols) } else { @@ -654,10 +982,10 @@ impl Renderer { for col in 0..vis_cols { let cell = state.visual_cell(row, col); - let mut fg = cell.attrs.fg; - let mut bg = cell.attrs.bg; + let mut fg_color = cell.attrs.fg; + let mut bg_color = cell.attrs.bg; if cell.attrs.inverse { - std::mem::swap(&mut fg, &mut bg); + std::mem::swap(&mut fg_color, &mut bg_color); } let selected = if let Some((r0, c0, r1, c1)) = selection { @@ -670,128 +998,208 @@ impl Renderer { false }; - let px = col * cw; - let py = tby + row * ch; - // DEFAULT_BG cells are semi-transparent (vibrancy shows through). - // Any other background (custom colour, selection) is fully opaque. - let bg32 = if selected { + let px = col as f32 * cw; + let py = tby + row as f32 * ch; + + let bg = if selected { sel_bg - } else if bg == DEFAULT_BG { - ((BG_ALPHA as u32) << 24) | DEFAULT_BG.to_u32() } else { - bg.to_u32_opaque() + let a = if bg_color == DEFAULT_BG { + BG_ALPHA as f32 / 255. + } else { + 1. + }; + c2fa(bg_color, a) }; - for dy in 0..ch { - let y = py + dy; - if y >= bh { - break; - } - let base = y * bw; - for dx in 0..cw { - let x = px + dx; - if x < bw { - buf[base + x] = bg32; - } - } - } + push_rect(&mut bg_rects, px, py, cw, ch, bg); + let c = cell.c; if c != ' ' && c != '\0' { - // Block elements are drawn as direct pixel fills — fontdue - // can't rasterize them correctly. - if !Self::draw_block_char(buf, bw, bh, px, py, cw, ch, c, fg.to_u32_opaque()) { - self.blit(buf, bw, bh, px, py, c, fg); + let fg = c2f(fg_color); + if !Self::push_block_char(&mut block_rects, px, py, cw, ch, c, fg) { + self.emit_char(&mut glyphs, c, px, py, fg); } } } } - // ── 2. URL underlines (drawn when Cmd held) ─────────────────────────── + // ── URL underlines ──────────────────────────────────────────────────── if !url_underlines.is_empty() { - let u_col = Color::new(0x58, 0x9a, 0xdd).to_u32_opaque(); // muted blue + let u_col = rgb_f(0x58, 0x9a, 0xdd); for &(row, c0, c1) in url_underlines { - if row >= vis_rows { - continue; - } - let uy = tby + row * ch + ch.saturating_sub(2); - if uy >= bh { - continue; - } - for col in c0..c1.min(vis_cols) { - let base = uy * bw + col * cw; - for dx in 0..cw { - if base + dx < buf.len() { - buf[base + dx] = u_col; - } - } - } + if row >= vis_rows { continue; } + let uy = tby + row as f32 * ch + ch - 2.; + let x0 = c0 as f32 * cw; + let x1 = c1.min(vis_cols) as f32 * cw; + push_rect(&mut fg_rects, x0, uy, x1 - x0, 2., u_col); } } - // ── 3. Ghost text ───────────────────────────────────────────────────── - if !state.is_scrolled_back() - && let Some(g) = ghost { - let py = tby + state.cursor_row * ch; + // ── Ghost text ──────────────────────────────────────────────────────── + if !state.is_scrolled_back() { + if let Some(g) = ghost { + let py = tby + state.cursor_row as f32 * ch; for (i, c) in g.chars().enumerate() { let col = state.cursor_col + i; - if col >= vis_cols { - break; - } - self.blit(buf, bw, bh, col * cw, py, c, GHOST_COLOR); + if col >= vis_cols { break; } + self.emit_char(&mut glyphs, c, col as f32 * cw, py, c2f(GHOST_COLOR)); } } + } - // ── 3. Cursor ───────────────────────────────────────────────────────── + // ── Cursor (2px vertical bar) ───────────────────────────────────────── if !state.is_scrolled_back() && show_cursor && state.cursor_row < vis_rows && state.cursor_col < vis_cols { - let px = state.cursor_col * cw; - let py = tby + state.cursor_row * ch; - let c32 = CURSOR_COLOR.to_u32_opaque(); - for dy in 0..ch { - let y = py + dy; - if y >= bh { - break; - } - let base = y * bw; - for dx in 0..2 { - let x = px + dx; - if x < bw { - buf[base + x] = c32; - } - } - } + let px = state.cursor_col as f32 * cw; + let py = tby + state.cursor_row as f32 * ch; + push_rect(&mut fg_rects, px, py, 2., ch, c2f(CURSOR_COLOR)); } - // ── 4. Scrollbar ────────────────────────────────────────────────────── + // ── Scrollbar ───────────────────────────────────────────────────────── let sb_total = state.scrollback.len(); if sb_total > 0 { let total = sb_total + state.rows; - let thumb_h = ((term_h * vis_rows) / total).max(8).min(term_h); + let term_h_u = term_h as usize; + let vis_rows_u = vis_rows; + let thumb_h = ((term_h_u * vis_rows_u) / total).max(8).min(term_h_u); let view_top = sb_total.saturating_sub(state.viewport_offset); - let thumb_y = tby + (view_top * (term_h - thumb_h)) / total.max(1); + let thumb_y = tby + (view_top * (term_h_u - thumb_h)) as f32 / total.max(1) as f32; - let bar_x = bw.saturating_sub(3); - let track_col = Color::new(0x2a, 0x2a, 0x2a).to_u32_opaque(); + let bar_x = bw - 3.; + let track_col = rgb_f(0x2a, 0x2a, 0x2a); let thumb_col = if state.is_scrolled_back() { - Color::new(0x66, 0x66, 0x66).to_u32_opaque() + rgb_f(0x66, 0x66, 0x66) } else { - Color::new(0x44, 0x44, 0x44).to_u32_opaque() + rgb_f(0x44, 0x44, 0x44) }; - for y in tby..bh { - let color = if y >= thumb_y && y < thumb_y + thumb_h { - thumb_col - } else { - track_col - }; - if bar_x < bw { - buf[y * bw + bar_x] = color; - } - if bar_x + 1 < bw { - buf[y * bw + bar_x + 1] = color; - } + // Track + push_rect(&mut fg_rects, bar_x, tby, 2., bh - tby, track_col); + // Thumb + push_rect( + &mut fg_rects, + bar_x, + thumb_y, + 2., + thumb_h as f32, + thumb_col, + ); + } + + // ── Upload instance data ────────────────────────────────────────────── + + // Pack all rects: [bg_rects | block_rects | fg_rects] into rect_buf + let bg_count = bg_rects.len(); + let block_count = block_rects.len(); + let fg_count = fg_rects.len(); + let total_rects = bg_count + block_count + fg_count; + + self.ensure_rect_buf(total_rects.max(1)); + let glyph_count = glyphs.len(); + self.ensure_glyph_buf(glyph_count.max(1)); + + let ri_size = std::mem::size_of::(); + let gi_size = std::mem::size_of::(); + + if !bg_rects.is_empty() { + self.queue.write_buffer( + &self.rect_buf, + 0, + bytemuck::cast_slice(&bg_rects), + ); + } + if !block_rects.is_empty() { + self.queue.write_buffer( + &self.rect_buf, + (bg_count * ri_size) as u64, + bytemuck::cast_slice(&block_rects), + ); + } + if !fg_rects.is_empty() { + self.queue.write_buffer( + &self.rect_buf, + ((bg_count + block_count) * ri_size) as u64, + bytemuck::cast_slice(&fg_rects), + ); + } + if !glyphs.is_empty() { + self.queue.write_buffer( + &self.glyph_buf, + 0, + bytemuck::cast_slice(&glyphs), + ); + } + + // ── Record render pass ──────────────────────────────────────────────── + + let mut encoder = self.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: Some("frame") }, + ); + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("term_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: DEFAULT_BG.r as f64 / 255., + g: DEFAULT_BG.g as f64 / 255., + b: DEFAULT_BG.b as f64 / 255., + a: 1., + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_bind_group(0, &self.uni_bg, &[]); + pass.set_pipeline(&self.rect_pipeline); + + // Draw call 1: background rects (cell BGs + tab bar) + if bg_count > 0 { + pass.set_vertex_buffer( + 0, + self.rect_buf.slice(..(bg_count * ri_size) as u64), + ); + pass.draw(0..4, 0..bg_count as u32); + } + + // Draw call 2: block character fills (on top of BG) + if block_count > 0 { + let start = (bg_count * ri_size) as u64; + let end = start + (block_count * ri_size) as u64; + pass.set_vertex_buffer(0, self.rect_buf.slice(start..end)); + pass.draw(0..4, 0..block_count as u32); + } + + // Draw call 3: glyphs (alpha-blended over backgrounds) + if glyph_count > 0 { + pass.set_pipeline(&self.glyph_pipeline); + pass.set_bind_group(1, &self.atlas_bg, &[]); + pass.set_vertex_buffer( + 0, + self.glyph_buf.slice(..(glyph_count * gi_size) as u64), + ); + pass.draw(0..4, 0..glyph_count as u32); + } + + // Draw call 4: foreground overlays (cursor, underlines, scrollbar, tab UI) + if fg_count > 0 { + pass.set_pipeline(&self.rect_pipeline); + pass.set_bind_group(0, &self.uni_bg, &[]); + let start = ((bg_count + block_count) * ri_size) as u64; + let end = start + (fg_count * ri_size) as u64; + pass.set_vertex_buffer(0, self.rect_buf.slice(start..end)); + pass.draw(0..4, 0..fg_count as u32); } } + + self.queue.submit(std::iter::once(encoder.finish())); } } -- 2.51.2