Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bafaa1db6d |
No files matched your search
Generated
+229
-203
@@ -234,18 +234,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.10.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d"
|
||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.9.1"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
|
||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
@@ -274,6 +274,12 @@ dependencies = [
|
||||
"core2",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -283,15 +289,6 @@ dependencies = [
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "built"
|
||||
version = "0.8.0"
|
||||
@@ -394,9 +391,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.2"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
@@ -409,9 +406,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.13.1"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
|
||||
checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"termcolor",
|
||||
@@ -453,6 +450,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -466,8 +473,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types 0.1.3",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
]
|
||||
@@ -479,7 +486,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation 0.10.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -721,6 +739,15 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "font-types"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "font-types"
|
||||
version = "0.12.4"
|
||||
@@ -744,7 +771,7 @@ dependencies = [
|
||||
"objc2-core-text",
|
||||
"objc2-foundation 0.3.2",
|
||||
"parlance",
|
||||
"read-fonts",
|
||||
"read-fonts 0.41.0",
|
||||
"roxmltree",
|
||||
"smallvec",
|
||||
"windows",
|
||||
@@ -779,30 +806,6 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
@@ -857,9 +860,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "glow"
|
||||
version = "0.17.0"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5"
|
||||
checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"slotmap",
|
||||
@@ -890,6 +893,26 @@ dependencies = [
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpu-descriptor"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
|
||||
dependencies = [
|
||||
"bitflags 2.10.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.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
@@ -910,7 +933,7 @@ checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bytemuck",
|
||||
"read-fonts",
|
||||
"read-fonts 0.41.0",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@@ -949,6 +972,12 @@ 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 = "icu_collections"
|
||||
version = "2.3.0"
|
||||
@@ -1079,9 +1108,9 @@ checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
version = "0.25.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
@@ -1097,8 +1126,8 @@ dependencies = [
|
||||
"rayon",
|
||||
"rgb",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
"zune-core 0.5.0",
|
||||
"zune-jpeg 0.5.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1218,12 +1247,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.105"
|
||||
version = "0.3.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
|
||||
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -1252,9 +1280,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
version = "0.2.179"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
@@ -1347,6 +1375,15 @@ dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maybe-rayon"
|
||||
version = "0.1.1"
|
||||
@@ -1372,6 +1409,21 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "metal"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block",
|
||||
"core-graphics-types 0.2.0",
|
||||
"foreign-types",
|
||||
"log",
|
||||
"objc",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -1384,9 +1436,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
version = "0.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
@@ -1394,9 +1446,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "naga"
|
||||
version = "30.0.1"
|
||||
version = "28.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130"
|
||||
checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set",
|
||||
@@ -1405,11 +1457,11 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"codespan-reporting",
|
||||
"half",
|
||||
"hashbrown 0.17.1",
|
||||
"hashbrown 0.16.1",
|
||||
"hexf-parse",
|
||||
"indexmap",
|
||||
"libm",
|
||||
"log",
|
||||
"naga-types",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rustc-hash",
|
||||
@@ -1418,18 +1470,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "naga-types"
|
||||
version = "30.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49"
|
||||
dependencies = [
|
||||
"hashbrown 0.17.1",
|
||||
"indexmap",
|
||||
"rustc-hash",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -1554,6 +1594,15 @@ dependencies = [
|
||||
"syn 2.0.113",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||
dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
@@ -1586,13 +1635,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-data",
|
||||
"objc2-core-image",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-quartz-core 0.2.2",
|
||||
"objc2-quartz-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1614,7 +1663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -1626,7 +1675,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
|
||||
dependencies = [
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
@@ -1638,7 +1687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
@@ -1673,10 +1722,10 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
|
||||
dependencies = [
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal 0.2.2",
|
||||
"objc2-metal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1685,7 +1734,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
|
||||
dependencies = [
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-contacts",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -1714,7 +1763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"dispatch",
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
@@ -1748,7 +1797,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
|
||||
dependencies = [
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -1761,23 +1810,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-metal"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.2.2"
|
||||
@@ -1785,24 +1822,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-metal 0.3.2",
|
||||
"objc2-metal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1822,7 +1845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
@@ -1830,7 +1853,7 @@ dependencies = [
|
||||
"objc2-core-location",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-link-presentation",
|
||||
"objc2-quartz-core 0.2.2",
|
||||
"objc2-quartz-core",
|
||||
"objc2-symbols",
|
||||
"objc2-uniform-type-identifiers",
|
||||
"objc2-user-notifications",
|
||||
@@ -1842,7 +1865,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
|
||||
dependencies = [
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
@@ -1854,7 +1877,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"objc2 0.5.2",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation 0.2.2",
|
||||
@@ -1948,7 +1971,7 @@ dependencies = [
|
||||
"linebender_resource_handle",
|
||||
"parlance",
|
||||
"parley_data",
|
||||
"skrifa",
|
||||
"skrifa 0.44.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2252,9 +2275,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ravif"
|
||||
version = "0.13.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
|
||||
checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285"
|
||||
dependencies = [
|
||||
"avif-serialize",
|
||||
"imgref",
|
||||
@@ -2271,18 +2294,6 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-metal"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
|
||||
dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-quartz-core 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
@@ -2303,6 +2314,16 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eaa2941a4c05443ee3a7b26ab076a553c343ad5995230cc2b1d3e993bdc6345"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"font-types 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.41.0"
|
||||
@@ -2310,7 +2331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"font-types",
|
||||
"font-types 0.12.4",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
@@ -2485,6 +2506,16 @@ dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9eb0b904a04d09bd68c65d946617b8ff733009999050f3b851c32fb3cfb60e"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"read-fonts 0.36.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.44.0"
|
||||
@@ -2492,7 +2523,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"read-fonts",
|
||||
"read-fonts 0.41.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2552,9 +2583,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spirv"
|
||||
version = "0.4.0+sdk-1.4.341.0"
|
||||
version = "0.3.0+sdk-1.3.268.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f"
|
||||
checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
@@ -2583,7 +2614,7 @@ version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e"
|
||||
dependencies = [
|
||||
"skrifa",
|
||||
"skrifa 0.39.0",
|
||||
"yazi",
|
||||
"zeno",
|
||||
]
|
||||
@@ -2672,16 +2703,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.11.3"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
|
||||
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
|
||||
dependencies = [
|
||||
"fax",
|
||||
"flate2",
|
||||
"half",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"zune-jpeg",
|
||||
"zune-jpeg 0.4.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2854,9 +2885,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.128"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
|
||||
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -2867,19 +2898,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.78"
|
||||
version = "0.4.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
|
||||
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.128"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
|
||||
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -2887,22 +2921,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.128"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
|
||||
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
"syn 2.0.113",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.128"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
|
||||
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -3018,9 +3052,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.105"
|
||||
version = "0.3.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
|
||||
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -3044,9 +3078,9 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "wgpu"
|
||||
version = "30.0.1"
|
||||
version = "28.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e"
|
||||
checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 2.10.0",
|
||||
@@ -3054,7 +3088,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"document-features",
|
||||
"hashbrown 0.17.1",
|
||||
"hashbrown 0.16.1",
|
||||
"js-sys",
|
||||
"log",
|
||||
"naga",
|
||||
@@ -3074,9 +3108,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-core"
|
||||
version = "30.0.1"
|
||||
version = "28.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14"
|
||||
checksum = "8bb4c8b5db5f00e56f1f08869d870a0dff7c8bc7ebc01091fec140b0cf0211a9"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set",
|
||||
@@ -3085,11 +3119,10 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"document-features",
|
||||
"hashbrown 0.17.1",
|
||||
"hashbrown 0.16.1",
|
||||
"indexmap",
|
||||
"log",
|
||||
"naga",
|
||||
"naga-types",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
@@ -3102,70 +3135,66 @@ dependencies = [
|
||||
"wgpu-core-deps-emscripten",
|
||||
"wgpu-core-deps-windows-linux-android",
|
||||
"wgpu-hal",
|
||||
"wgpu-naga-bridge",
|
||||
"wgpu-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-core-deps-apple"
|
||||
version = "30.0.1"
|
||||
version = "28.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68"
|
||||
checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-core-deps-emscripten"
|
||||
version = "30.0.1"
|
||||
version = "28.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86"
|
||||
checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-core-deps-windows-linux-android"
|
||||
version = "30.0.1"
|
||||
version = "28.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f"
|
||||
checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-hal"
|
||||
version = "30.0.1"
|
||||
version = "28.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58"
|
||||
checksum = "293080d77fdd14d6b08a67c5487dfddbf874534bb7921526db56a7b75d7e3bef"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"arrayvec",
|
||||
"ash",
|
||||
"bit-set",
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"block",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"core-graphics-types 0.2.0",
|
||||
"glow",
|
||||
"glutin_wgl_sys",
|
||||
"gpu-allocator",
|
||||
"hashbrown 0.17.1",
|
||||
"gpu-descriptor",
|
||||
"hashbrown 0.16.1",
|
||||
"js-sys",
|
||||
"khronos-egl",
|
||||
"libc",
|
||||
"libloading",
|
||||
"log",
|
||||
"metal",
|
||||
"naga",
|
||||
"naga-types",
|
||||
"ndk-sys",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-metal 0.3.2",
|
||||
"objc2-quartz-core 0.3.2",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"ordered-float",
|
||||
"parking_lot",
|
||||
@@ -3174,44 +3203,26 @@ dependencies = [
|
||||
"profiling",
|
||||
"range-alloc",
|
||||
"raw-window-handle",
|
||||
"raw-window-metal",
|
||||
"renderdoc-sys",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.17",
|
||||
"wasm-bindgen",
|
||||
"wayland-sys",
|
||||
"web-sys",
|
||||
"wgpu-naga-bridge",
|
||||
"wgpu-types",
|
||||
"windows",
|
||||
"windows-core",
|
||||
"windows-result",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-naga-bridge"
|
||||
version = "30.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081"
|
||||
dependencies = [
|
||||
"naga",
|
||||
"wgpu-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-types"
|
||||
version = "30.0.1"
|
||||
version = "28.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286"
|
||||
checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bytemuck",
|
||||
"js-sys",
|
||||
"log",
|
||||
"naga-types",
|
||||
"raw-window-handle",
|
||||
"static_assertions",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
@@ -3575,12 +3586,12 @@ dependencies = [
|
||||
"android-activity",
|
||||
"atomic-waker",
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.5.1",
|
||||
"block2",
|
||||
"bytemuck",
|
||||
"calloop",
|
||||
"cfg_aliases",
|
||||
"concurrent-queue",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics",
|
||||
"cursor-icon",
|
||||
"dpi",
|
||||
@@ -3847,6 +3858,12 @@ dependencies = [
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.0"
|
||||
@@ -3862,11 +3879,20 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
|
||||
dependencies = [
|
||||
"zune-core 0.4.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
"zune-core 0.5.0",
|
||||
]
|
||||
+2
-2
@@ -29,9 +29,9 @@ edition = "2024"
|
||||
[workspace.dependencies]
|
||||
pollster = "0.4.0"
|
||||
winit = "0.30.12"
|
||||
wgpu = "30.0.1"
|
||||
wgpu = "28.0.0"
|
||||
bytemuck = "1.23.1"
|
||||
image = "0.25.10"
|
||||
image = "0.25.6"
|
||||
parley = "0.11.1"
|
||||
swash = "0.2.10"
|
||||
fxhash = "0.2.1"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
use crate::{
|
||||
render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||
UiRegion, WidgetId,
|
||||
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
|
||||
util::to_mut,
|
||||
};
|
||||
|
||||
@@ -39,7 +40,7 @@ struct Child {
|
||||
tail: usize,
|
||||
}
|
||||
|
||||
pub type DrawLayers = Layers<LayerDraws>;
|
||||
pub type PrimitiveLayers = Layers<Primitives>;
|
||||
|
||||
impl<T: Default> Layers<T> {
|
||||
pub fn new() -> Layers<T> {
|
||||
@@ -119,7 +120,7 @@ impl<T: Default> Layers<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawLayers {
|
||||
impl PrimitiveLayers {
|
||||
pub fn write<P: Primitive>(
|
||||
&mut self,
|
||||
layer: LayerId,
|
||||
@@ -131,6 +132,17 @@ impl DrawLayers {
|
||||
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
||||
self[h.layer].free(h)
|
||||
}
|
||||
|
||||
pub fn write_image(
|
||||
&mut self,
|
||||
layer: LayerId,
|
||||
id: WidgetId,
|
||||
texture_idx: u32,
|
||||
region: UiRegion,
|
||||
mask_idx: MaskIdx,
|
||||
) -> PrimitiveHandle {
|
||||
self[layer].write_image(layer, id, texture_idx, region, mask_idx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> Default for Layers<T> {
|
||||
|
||||
+22
-13
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
|
||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor,
|
||||
util::Vec2,
|
||||
};
|
||||
use parley::{
|
||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
||||
@@ -158,7 +159,7 @@ impl TextBuffer {
|
||||
}
|
||||
|
||||
impl TextData {
|
||||
pub fn place(&mut self, buffer: &TextBuffer) -> Vec<PlacedGlyph> {
|
||||
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
|
||||
let mut placed = Vec::new();
|
||||
for line in buffer.layout.lines() {
|
||||
for item in line.items() {
|
||||
@@ -184,14 +185,17 @@ impl TextData {
|
||||
subpixel,
|
||||
coords: coords_hash,
|
||||
};
|
||||
let Some(entry) = self.glyph_entry(GlyphRaster {
|
||||
key,
|
||||
font: font_ref,
|
||||
font_size,
|
||||
coords,
|
||||
subpixel,
|
||||
glyph_id: glyph.id,
|
||||
}) else {
|
||||
let Some(entry) = self.glyph_entry(
|
||||
GlyphRaster {
|
||||
key,
|
||||
font: font_ref,
|
||||
font_size,
|
||||
coords,
|
||||
subpixel,
|
||||
glyph_id: glyph.id,
|
||||
},
|
||||
textures,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
placed.push(PlacedGlyph {
|
||||
@@ -207,7 +211,11 @@ impl TextData {
|
||||
placed
|
||||
}
|
||||
|
||||
fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option<GlyphEntry> {
|
||||
fn glyph_entry(
|
||||
&mut self,
|
||||
glyph: GlyphRaster<'_>,
|
||||
textures: &mut Textures,
|
||||
) -> Option<GlyphEntry> {
|
||||
if let Some(entry) = self.atlas.get(&glyph.key) {
|
||||
return entry;
|
||||
}
|
||||
@@ -229,7 +237,7 @@ impl TextData {
|
||||
.render(&mut scaler, glyph.glyph_id as u16);
|
||||
|
||||
if let Some(image) = image {
|
||||
self.atlas.insert(glyph.key, &image)
|
||||
self.atlas.insert(glyph.key, &image, textures)
|
||||
} else {
|
||||
self.atlas.insert_empty(glyph.key);
|
||||
None
|
||||
@@ -270,9 +278,10 @@ impl TextData {
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
textures: &mut Textures,
|
||||
) -> RenderedText {
|
||||
buffer.shape(self, attrs, width);
|
||||
let glyphs = self.place(buffer);
|
||||
let glyphs = self.place(buffer, textures);
|
||||
RenderedText {
|
||||
glyphs,
|
||||
size: buffer.size(),
|
||||
|
||||
@@ -5,12 +5,28 @@ use std::{
|
||||
sync::mpsc::{Receiver, Sender, channel},
|
||||
};
|
||||
|
||||
/// Which of the two things a texture slot holds. The two are drawn very
|
||||
/// differently: a page is a layer of one shared array texture and never gets
|
||||
/// its own bind group; a standalone image is the opposite, one texture and
|
||||
/// one bind group, never a layer.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TextureKind {
|
||||
Image,
|
||||
/// The array-texture layer this page was assigned. Chosen synchronously
|
||||
/// by `Textures::add_page` rather than by the renderer, because glyph
|
||||
/// insertion needs it in the same call, before any GPU sync happens.
|
||||
Page {
|
||||
layer: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextureHandle {
|
||||
slot: u32,
|
||||
kind: TextureKind,
|
||||
size: Vec2,
|
||||
counter: RefCounter,
|
||||
send: Sender<u32>,
|
||||
send: Sender<(TextureKind, u32)>,
|
||||
}
|
||||
|
||||
/// a texture manager for a ui
|
||||
@@ -18,19 +34,24 @@ pub struct TextureHandle {
|
||||
pub struct Textures {
|
||||
free: Vec<u32>,
|
||||
images: Vec<Option<DynamicImage>>,
|
||||
/// Next layer to hand out to an atlas page. Pages are never freed (no
|
||||
/// atlas eviction), so this only grows and `free` never holds one.
|
||||
next_page_layer: u32,
|
||||
updates: Vec<Update>,
|
||||
send: Sender<u32>,
|
||||
recv: Receiver<u32>,
|
||||
send: Sender<(TextureKind, u32)>,
|
||||
recv: Receiver<(TextureKind, u32)>,
|
||||
}
|
||||
|
||||
pub enum TextureUpdate<'a> {
|
||||
Push(&'a DynamicImage),
|
||||
Set(u32, &'a DynamicImage),
|
||||
Push(TextureKind, &'a DynamicImage),
|
||||
Set(TextureKind, u32, &'a DynamicImage),
|
||||
/// Overwrite a rectangle of an existing texture, rather than replacing it.
|
||||
/// The glyph atlas grows a glyph at a time, and re-uploading a whole atlas
|
||||
/// per glyph is megabytes of copy for a few hundred bytes of change.
|
||||
/// Only ever issued against a page -- a standalone image is never patched.
|
||||
Patch(u32, PatchRect, &'a DynamicImage),
|
||||
Free(u32),
|
||||
/// Added and freed before the renderer drained either update. It still has
|
||||
/// to push a slot to stay lined up with `images`; `Free` then empties it.
|
||||
PushFree,
|
||||
PushFree(TextureKind),
|
||||
SetFree,
|
||||
}
|
||||
|
||||
@@ -43,8 +64,8 @@ pub struct PatchRect {
|
||||
}
|
||||
|
||||
enum Update {
|
||||
Push(u32),
|
||||
Set(u32),
|
||||
Push(TextureKind, u32),
|
||||
Set(TextureKind, u32),
|
||||
Patch(u32, PatchRect),
|
||||
Free(u32),
|
||||
}
|
||||
@@ -55,31 +76,54 @@ impl Textures {
|
||||
Self {
|
||||
free: Vec::new(),
|
||||
images: Vec::new(),
|
||||
next_page_layer: 0,
|
||||
updates: Vec::new(),
|
||||
send,
|
||||
recv,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
||||
let image = image.into();
|
||||
let size = image.dimensions().into();
|
||||
let kind = TextureKind::Image;
|
||||
let slot = self.push(kind, image);
|
||||
TextureHandle {
|
||||
slot: self.push(image),
|
||||
slot,
|
||||
kind,
|
||||
size,
|
||||
counter: RefCounter::new(),
|
||||
send: self.send.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, image: DynamicImage) -> u32 {
|
||||
/// Adds a page of the shared glyph atlas array. Only `atlas.rs` should
|
||||
/// call this -- everything else wants `add`.
|
||||
pub fn add_page(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
||||
let image = image.into();
|
||||
let size = image.dimensions().into();
|
||||
let layer = self.next_page_layer;
|
||||
self.next_page_layer += 1;
|
||||
let kind = TextureKind::Page { layer };
|
||||
let slot = self.push(kind, image);
|
||||
TextureHandle {
|
||||
slot,
|
||||
kind,
|
||||
size,
|
||||
counter: RefCounter::new(),
|
||||
send: self.send.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, kind: TextureKind, image: DynamicImage) -> u32 {
|
||||
if let Some(i) = self.free.pop() {
|
||||
self.images[i as usize] = Some(image);
|
||||
self.updates.push(Update::Set(i));
|
||||
self.updates.push(Update::Set(kind, i));
|
||||
i
|
||||
} else {
|
||||
let i = self.images.len() as u32;
|
||||
self.images.push(Some(image));
|
||||
self.updates.push(Update::Push(i));
|
||||
self.updates.push(Update::Push(kind, i));
|
||||
i
|
||||
}
|
||||
}
|
||||
@@ -95,29 +139,30 @@ impl Textures {
|
||||
self.updates.push(Update::Patch(handle.slot, rect));
|
||||
}
|
||||
|
||||
/// How many textures are live, which is what a ui can ask; the renderer's
|
||||
/// copies follow from the updates it drains.
|
||||
pub fn count(&self) -> usize {
|
||||
self.images.iter().flatten().count()
|
||||
}
|
||||
|
||||
pub fn free(&mut self) {
|
||||
for idx in self.recv.try_iter() {
|
||||
for (kind, idx) in self.recv.try_iter() {
|
||||
self.images[idx as usize] = None;
|
||||
self.updates.push(Update::Free(idx));
|
||||
self.free.push(idx);
|
||||
// A page's slot is never reclaimed: `GlyphAtlas` never drops the
|
||||
// handles it holds, and there is no eviction path for a hole in
|
||||
// the middle of the array's layers. So `free` holds ordinary
|
||||
// image slots only, and a page's layer would need a free list of
|
||||
// its own were that to change.
|
||||
if kind == TextureKind::Image {
|
||||
self.free.push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn updates(&mut self) -> impl Iterator<Item = TextureUpdate<'_>> {
|
||||
self.updates.drain(..).map(|u| match u {
|
||||
Update::Push(i) => self.images[i as usize]
|
||||
Update::Push(kind, i) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(TextureUpdate::Push)
|
||||
.unwrap_or(TextureUpdate::PushFree),
|
||||
Update::Set(i) => self.images[i as usize]
|
||||
.map(|img| TextureUpdate::Push(kind, img))
|
||||
.unwrap_or(TextureUpdate::PushFree(kind)),
|
||||
Update::Set(kind, i) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Set(i, img))
|
||||
.map(|img| TextureUpdate::Set(kind, i, img))
|
||||
.unwrap_or(TextureUpdate::SetFree),
|
||||
Update::Patch(i, rect) => self.images[i as usize]
|
||||
.as_ref()
|
||||
@@ -129,19 +174,36 @@ impl Textures {
|
||||
}
|
||||
|
||||
impl TextureHandle {
|
||||
/// Index into `Textures`, and into the renderer's parallel slots.
|
||||
pub fn slot(&self) -> u32 {
|
||||
self.slot
|
||||
}
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// The bind-group index this handle draws with. Only valid for a
|
||||
/// standalone image; an atlas page has no bind group of its own -- it
|
||||
/// samples the shared array via `layer()` instead. Getting this wrong is
|
||||
/// a caller bug (the wrong kind of handle reached the wrong draw path),
|
||||
/// not a recoverable condition, so it panics rather than drawing garbage.
|
||||
pub fn image_index(&self) -> u32 {
|
||||
match self.kind {
|
||||
TextureKind::Image => self.slot,
|
||||
TextureKind::Page { .. } => panic!("image_index() called on an atlas page handle"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The layer this page occupies in the shared atlas array texture.
|
||||
/// Only valid for a page handle; see `image_index`'s note.
|
||||
pub fn layer(&self) -> u32 {
|
||||
match self.kind {
|
||||
TextureKind::Page { layer } => layer,
|
||||
TextureKind::Image => panic!("layer() called on a standalone image handle"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TextureHandle {
|
||||
fn drop(&mut self) {
|
||||
if self.counter.drop() {
|
||||
let _ = self.send.send(self.slot);
|
||||
let _ = self.send.send((self.kind, self.slot));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-47
@@ -1,11 +1,15 @@
|
||||
use crate::{
|
||||
PatchRect,
|
||||
PatchRect, TextureHandle, Textures,
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use image::RgbaImage;
|
||||
use swash::scale::image::{Content, Image};
|
||||
|
||||
/// Side of one page, and so of every layer of `render::page`'s array texture.
|
||||
/// Side of one atlas page, in pixels. 1024 is 4 MB at RGBA8 -- enough for a
|
||||
/// few thousand glyphs at UI sizes, and small enough that a page nobody fills
|
||||
/// is not a big waste. Also the fixed width/height of every layer of the
|
||||
/// shared array texture in `render::texture` -- `pub(crate)` so that module
|
||||
/// can size it without a second constant to keep in sync.
|
||||
pub(crate) const PAGE: u32 = 1024;
|
||||
|
||||
/// Transparent margin kept around every glyph, so that sampling one cannot
|
||||
@@ -36,7 +40,7 @@ pub struct GlyphEntry {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub is_colored: bool,
|
||||
/// Which atlas array layer this glyph is on.
|
||||
/// The atlas array layer this glyph's page occupies.
|
||||
pub layer: u32,
|
||||
}
|
||||
|
||||
@@ -49,26 +53,18 @@ impl GlyphEntry {
|
||||
}
|
||||
|
||||
struct Page {
|
||||
image: RgbaImage,
|
||||
handle: TextureHandle,
|
||||
x: u32,
|
||||
y: u32,
|
||||
shelf_height: u32,
|
||||
}
|
||||
|
||||
/// A rectangle of one page the renderer has not uploaded yet.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PageUpload {
|
||||
pub layer: u32,
|
||||
pub rect: PatchRect,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GlyphAtlas {
|
||||
pages: Vec<Page>,
|
||||
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
||||
/// too, so it is not re-rasterised on every layout.
|
||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||
uploads: Vec<PageUpload>,
|
||||
}
|
||||
|
||||
impl GlyphAtlas {
|
||||
@@ -76,7 +72,12 @@ impl GlyphAtlas {
|
||||
self.entries.get(key).copied()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option<GlyphEntry> {
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
key: GlyphKey,
|
||||
image: &Image,
|
||||
textures: &mut Textures,
|
||||
) -> Option<GlyphEntry> {
|
||||
let w = image.placement.width;
|
||||
let h = image.placement.height;
|
||||
if w == 0 || h == 0 {
|
||||
@@ -98,11 +99,23 @@ impl GlyphAtlas {
|
||||
return None;
|
||||
}
|
||||
|
||||
let upload = self.allocate(w, h);
|
||||
let PatchRect { x, y, .. } = upload.rect;
|
||||
write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y);
|
||||
self.uploads.push(upload);
|
||||
let (page_idx, x, y) = self.allocate(w, h, textures);
|
||||
let page = &self.pages[page_idx];
|
||||
|
||||
let img = textures.image_mut(&page.handle);
|
||||
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
|
||||
write_glyph(rgba, image, x, y);
|
||||
|
||||
let handle = page.handle.clone();
|
||||
let rect = PatchRect {
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
};
|
||||
textures.patch(&handle, rect);
|
||||
|
||||
let page = &self.pages[page_idx];
|
||||
let scale = 1.0 / PAGE as f32;
|
||||
let entry = GlyphEntry {
|
||||
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
|
||||
@@ -112,60 +125,38 @@ impl GlyphAtlas {
|
||||
width: w,
|
||||
height: h,
|
||||
is_colored: matches!(image.content, Content::Color),
|
||||
layer: upload.layer,
|
||||
layer: page.handle.layer(),
|
||||
};
|
||||
self.entries.insert(key, Some(entry));
|
||||
Some(entry)
|
||||
}
|
||||
|
||||
/// Reserves room for a `w` by `h` glyph, adding a page if none has it.
|
||||
fn allocate(&mut self, w: u32, h: u32) -> PageUpload {
|
||||
let rect = |x, y| PatchRect {
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
};
|
||||
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
|
||||
if let Some((i, (x, y))) = self
|
||||
.pages
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
|
||||
{
|
||||
return PageUpload {
|
||||
layer: i as u32,
|
||||
rect: rect(x, y),
|
||||
};
|
||||
return (i, x, y);
|
||||
}
|
||||
|
||||
let handle = textures.add_page(RgbaImage::new(PAGE, PAGE));
|
||||
self.pages.push(Page {
|
||||
image: RgbaImage::new(PAGE, PAGE),
|
||||
handle,
|
||||
x: PAD + w + PAD,
|
||||
y: PAD,
|
||||
shelf_height: h + PAD,
|
||||
});
|
||||
PageUpload {
|
||||
layer: self.pages.len() as u32 - 1,
|
||||
rect: rect(PAD, PAD),
|
||||
}
|
||||
}
|
||||
|
||||
/// Drains what has been written since the last call, for the renderer to
|
||||
/// upload. A new page needs nothing more: wgpu leaves the rest of a fresh
|
||||
/// layer transparent, which is what an atlas wants.
|
||||
pub fn uploads(&mut self) -> impl Iterator<Item = (PageUpload, &RgbaImage)> {
|
||||
let pages = &self.pages;
|
||||
self.uploads
|
||||
.drain(..)
|
||||
.map(|upload| (upload, &pages[upload.layer as usize].image))
|
||||
(self.pages.len() - 1, PAD, PAD)
|
||||
}
|
||||
|
||||
pub fn insert_empty(&mut self, key: GlyphKey) {
|
||||
self.entries.insert(key, None);
|
||||
}
|
||||
|
||||
pub fn page_count(&self) -> u32 {
|
||||
self.pages.len() as u32
|
||||
pub fn page_count(&self) -> usize {
|
||||
self.pages.len()
|
||||
}
|
||||
|
||||
pub fn glyph_count(&self) -> usize {
|
||||
|
||||
@@ -12,16 +12,20 @@ pub struct WindowUniform {
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct PrimitiveInstance {
|
||||
pub region: UiRegion,
|
||||
pub binding: u32,
|
||||
pub idx: u32,
|
||||
pub mask_idx: MaskIdx,
|
||||
}
|
||||
|
||||
impl PrimitiveInstance {
|
||||
const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
|
||||
const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![
|
||||
0 => Float32x2,
|
||||
1 => Float32x2,
|
||||
2 => Float32x2,
|
||||
3 => Float32x2,
|
||||
4 => Uint32,
|
||||
5 => Uint32,
|
||||
6 => Uint32,
|
||||
];
|
||||
|
||||
pub fn desc() -> VertexBufferLayout<'static> {
|
||||
|
||||
+241
-211
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
UiData, UiRenderState,
|
||||
render::{data::PrimitiveInstance, util::ArrBuf},
|
||||
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use data::WindowUniform;
|
||||
@@ -11,7 +11,6 @@ use wgpu::{
|
||||
|
||||
mod atlas;
|
||||
mod data;
|
||||
mod page;
|
||||
mod primitive;
|
||||
mod texture;
|
||||
mod util;
|
||||
@@ -20,62 +19,64 @@ pub use atlas::*;
|
||||
pub use data::{Mask, MaskIdx};
|
||||
pub use primitive::*;
|
||||
|
||||
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
||||
|
||||
pub struct UiRenderNode {
|
||||
shared_layout: BindGroupLayout,
|
||||
shared_group: BindGroup,
|
||||
format: TextureFormat,
|
||||
uniform_group: BindGroup,
|
||||
primitive_layout: BindGroupLayout,
|
||||
rsc_layout: BindGroupLayout,
|
||||
rsc_group: BindGroup,
|
||||
|
||||
/// One per registered primitive, in id order.
|
||||
primitives: Vec<PrimitivePipeline>,
|
||||
pipeline: RenderPipeline,
|
||||
|
||||
layers: HashMap<usize, RenderLayer>,
|
||||
active: Vec<usize>,
|
||||
window_buffer: Buffer,
|
||||
textures: GpuTextures,
|
||||
masks: ArrBuf<Mask>,
|
||||
}
|
||||
|
||||
struct RenderLayer {
|
||||
/// One per registered primitive, `None` where this layer draws none.
|
||||
primitives: Vec<Option<ListBuffers>>,
|
||||
}
|
||||
|
||||
/// What draws one registered primitive.
|
||||
struct PrimitivePipeline {
|
||||
data_layout: BindGroupLayout,
|
||||
pipeline: RenderPipeline,
|
||||
render: Box<dyn PrimitiveRender>,
|
||||
}
|
||||
|
||||
/// One list's vertex buffer and the data its shader reads.
|
||||
struct ListBuffers {
|
||||
instance: ArrBuf<PrimitiveInstance>,
|
||||
data: ArrBuf<u8>,
|
||||
group: Option<BindGroup>,
|
||||
/// What the primitive asked to keep per instance, if anything.
|
||||
bindings: Vec<u32>,
|
||||
primitives: PrimitiveBuffers,
|
||||
primitive_group: BindGroup,
|
||||
/// A standalone image's instances, kept apart from `instance` because
|
||||
/// each one draws with its own bind group -- see `UiRenderNode::draw`.
|
||||
image_instance: ArrBuf<PrimitiveInstance>,
|
||||
/// The texture slot each entry of `image_instance` draws with, in the
|
||||
/// same order, refreshed alongside it. Not stored in the vertex buffer
|
||||
/// itself because it names a bind group, not shader data.
|
||||
image_tex_indices: Vec<u32>,
|
||||
}
|
||||
|
||||
impl UiRenderNode {
|
||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||
pass.set_bind_group(0, &self.shared_group, &[]);
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_group, &[]);
|
||||
for i in &self.active {
|
||||
let layer = &self.layers[i];
|
||||
for (id, list) in layer.primitives.iter().enumerate() {
|
||||
let Some(list) = list else { continue };
|
||||
let Some(group) = &list.group else { continue };
|
||||
let primitive = &self.primitives[id];
|
||||
pass.set_pipeline(&primitive.pipeline);
|
||||
pass.set_bind_group(1, group, &[]);
|
||||
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
|
||||
primitive.render.draw(
|
||||
pass,
|
||||
ListDraw {
|
||||
instances: list.instance.len() as u32,
|
||||
bindings: &list.bindings,
|
||||
},
|
||||
);
|
||||
if layer.instance.len() == 0 && layer.image_instance.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
pass.set_bind_group(1, &layer.primitive_group, &[]);
|
||||
if layer.instance.len() > 0 {
|
||||
pass.set_bind_group(2, &self.rsc_group, &[]);
|
||||
pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
|
||||
pass.draw(0..4, 0..layer.instance.len() as u32);
|
||||
}
|
||||
// Images draw after this layer's rects and glyphs, one draw call
|
||||
// each with its own bind group. That draws every image "on top"
|
||||
// within the layer, which loses nothing that currently exists:
|
||||
// `Primitives::apply_free` frees with `swap_remove`, so a layer's
|
||||
// draw order was already undefined before images had their own
|
||||
// list -- nothing before this relied on interleaving a rect
|
||||
// between two images at a particular position.
|
||||
if layer.image_instance.len() > 0 {
|
||||
pass.set_vertex_buffer(0, layer.image_instance.buffer.slice(..));
|
||||
for (k, &tex_idx) in layer.image_tex_indices.iter().enumerate() {
|
||||
pass.set_bind_group(2, self.textures.image_bind_group(tex_idx), &[]);
|
||||
pass.draw(0..4, k as u32..k as u32 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,57 +88,83 @@ impl UiRenderNode {
|
||||
ui: &mut UiData,
|
||||
ui_render: &mut UiRenderState,
|
||||
) {
|
||||
// Before the layers: each list is given its pipeline's data layout.
|
||||
self.build_pipelines(device, queue, &ui.primitives);
|
||||
self.active.clear();
|
||||
for (i, draws) in ui_render.layers.iter_mut() {
|
||||
for (i, primitives) in ui_render.layers.iter_mut() {
|
||||
self.active.push(i);
|
||||
for change in draws.apply_free() {
|
||||
for change in primitives.apply_free() {
|
||||
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
||||
for h in &mut inst.primitives {
|
||||
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
|
||||
// `is_image` disambiguates: `instances` and `images`
|
||||
// are separate lists with independent indices, so
|
||||
// without it a rect's renumbering could be applied to
|
||||
// an image handle that happened to share the same
|
||||
// (layer, inst_idx).
|
||||
if h.layer == i
|
||||
&& h.inst_idx == change.old
|
||||
&& (h.binding == IMAGE_BINDING) == change.is_image
|
||||
{
|
||||
h.inst_idx = change.new;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new);
|
||||
if draws.updated {
|
||||
let lists = draws.primitives();
|
||||
// The zip would otherwise skip a list with no pipeline.
|
||||
assert!(lists.len() <= self.primitives.len());
|
||||
rlayer.primitives.resize_with(lists.len(), || None);
|
||||
for ((buffers, list), primitive) in rlayer
|
||||
.primitives
|
||||
.iter_mut()
|
||||
.zip(lists)
|
||||
.zip(&self.primitives)
|
||||
{
|
||||
let Some(list) = list else {
|
||||
continue;
|
||||
};
|
||||
buffers
|
||||
.get_or_insert_with(|| ListBuffers::new(device))
|
||||
.update(device, queue, primitive, list);
|
||||
let rlayer = self.layers.entry(i).or_insert_with(|| {
|
||||
let primitives = PrimitiveBuffers::new(device);
|
||||
let primitive_group =
|
||||
Self::primitive_group(device, &self.primitive_layout, primitives.buffers());
|
||||
RenderLayer {
|
||||
instance: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
||||
"instance",
|
||||
),
|
||||
primitives,
|
||||
primitive_group,
|
||||
image_instance: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
||||
"image instance",
|
||||
),
|
||||
image_tex_indices: Vec::new(),
|
||||
}
|
||||
draws.updated = false;
|
||||
}
|
||||
}
|
||||
for primitive in &mut self.primitives {
|
||||
primitive.render.update(ui);
|
||||
}
|
||||
if ui.masks.changed {
|
||||
ui.masks.changed = false;
|
||||
if self.masks.update(device, queue, &ui.masks[..]) {
|
||||
self.shared_group = Self::shared_group(
|
||||
});
|
||||
if primitives.updated {
|
||||
rlayer
|
||||
.instance
|
||||
.update(device, queue, primitives.instances());
|
||||
rlayer.primitives.update(device, queue, primitives.data());
|
||||
rlayer.primitive_group = Self::primitive_group(
|
||||
device,
|
||||
&self.shared_layout,
|
||||
&self.window_buffer,
|
||||
&self.masks,
|
||||
&self.primitive_layout,
|
||||
rlayer.primitives.buffers(),
|
||||
);
|
||||
rlayer
|
||||
.image_instance
|
||||
.update(device, queue, primitives.image_instances());
|
||||
rlayer.image_tex_indices = primitives
|
||||
.image_instances()
|
||||
.iter()
|
||||
.map(|inst| inst.idx)
|
||||
.collect();
|
||||
primitives.updated = false;
|
||||
}
|
||||
}
|
||||
let masks_resized = if ui.masks.changed {
|
||||
ui.masks.changed = false;
|
||||
self.masks.update(device, queue, &ui.masks[..])
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let rebuild_main = self.textures.update(
|
||||
&mut ui.textures,
|
||||
&self.rsc_layout,
|
||||
&self.masks,
|
||||
masks_resized,
|
||||
);
|
||||
if rebuild_main {
|
||||
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
||||
@@ -149,7 +176,12 @@ impl UiRenderNode {
|
||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||
}
|
||||
|
||||
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
||||
pub fn new(device: &Device, queue: &Queue, config: &SurfaceConfiguration) -> Self {
|
||||
let shader = device.create_shader_module(ShaderModuleDescriptor {
|
||||
label: Some("UI Shape Shader"),
|
||||
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
|
||||
});
|
||||
|
||||
let window_uniform = WindowUniform {
|
||||
width: config.width as f32,
|
||||
height: config.height as f32,
|
||||
@@ -160,73 +192,65 @@ impl UiRenderNode {
|
||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let shared_layout = Self::shared_layout(device);
|
||||
let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
label: Some("window"),
|
||||
});
|
||||
|
||||
let uniform_group = Self::bind_group_0(device, &uniform_layout, &window_buffer);
|
||||
|
||||
let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &PrimitiveBuffers::BINDINGS.map(|binding| BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}),
|
||||
label: Some("primitive"),
|
||||
});
|
||||
|
||||
let tex_manager = GpuTextures::new(device, queue);
|
||||
let masks = ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui masks",
|
||||
);
|
||||
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
|
||||
|
||||
Self {
|
||||
shared_layout,
|
||||
shared_group,
|
||||
format: config.format,
|
||||
primitives: Vec::new(),
|
||||
window_buffer,
|
||||
layers: HashMap::default(),
|
||||
active: Vec::new(),
|
||||
masks,
|
||||
}
|
||||
}
|
||||
let rsc_layout = Self::rsc_layout(device);
|
||||
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
|
||||
|
||||
/// Compiles a pipeline for every primitive registered since the last call.
|
||||
/// Sources only ever arrive at the end, so an id keeps its pipeline.
|
||||
fn build_pipelines(&mut self, device: &Device, queue: &Queue, registry: &PrimitiveRegistry) {
|
||||
for source in ®istry.sources()[self.primitives.len()..] {
|
||||
let render = (source.render)(device, queue);
|
||||
let data_layout = Self::data_layout(device, source.stride);
|
||||
let mut groups = vec![Some(&self.shared_layout), Some(&data_layout)];
|
||||
groups.extend(render.layout().map(Some));
|
||||
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some(source.label),
|
||||
bind_group_layouts: &groups,
|
||||
immediate_size: 0,
|
||||
});
|
||||
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
|
||||
self.primitives.push(PrimitivePipeline {
|
||||
data_layout,
|
||||
pipeline,
|
||||
render,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn pipeline(
|
||||
device: &Device,
|
||||
layout: &PipelineLayout,
|
||||
format: TextureFormat,
|
||||
wgsl: &str,
|
||||
label: &str,
|
||||
) -> RenderPipeline {
|
||||
let module = device.create_shader_module(ShaderModuleDescriptor {
|
||||
label: Some(label),
|
||||
source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()),
|
||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some("UI Shape Pipeline Layout"),
|
||||
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
|
||||
immediate_size: 0,
|
||||
});
|
||||
device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(layout),
|
||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: Some("UI Shape Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: VertexState {
|
||||
module: &module,
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[Some(PrimitiveInstance::desc())],
|
||||
buffers: &[PrimitiveInstance::desc()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: &module,
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(ColorTargetState {
|
||||
format,
|
||||
format: config.format,
|
||||
blend: Some(BlendState::ALPHA_BLENDING),
|
||||
write_mask: ColorWrites::ALL,
|
||||
})],
|
||||
@@ -249,42 +273,107 @@ impl UiRenderNode {
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
Self {
|
||||
uniform_group,
|
||||
primitive_layout,
|
||||
rsc_layout,
|
||||
rsc_group,
|
||||
pipeline,
|
||||
window_buffer,
|
||||
layers: HashMap::default(),
|
||||
active: Vec::new(),
|
||||
textures: tex_manager,
|
||||
masks,
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_group_0(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
window_buffer: &Buffer,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: window_buffer.as_entire_binding(),
|
||||
}],
|
||||
label: Some("ui window"),
|
||||
})
|
||||
}
|
||||
|
||||
/// What every draw in the ui is given: the window and the masks.
|
||||
fn shared_layout(device: &Device) -> BindGroupLayout {
|
||||
fn primitive_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
buffers: [(u32, &Buffer); PrimitiveBuffers::LEN],
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &buffers.map(|(binding, buf)| BindGroupEntry {
|
||||
binding,
|
||||
resource: buf.as_entire_binding(),
|
||||
}),
|
||||
label: Some("ui primitives"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Group 2: the shared atlas array, one standalone-image slot (a null
|
||||
/// view for the main draw, a real one for each image's own bind group --
|
||||
/// see `GpuTextures`), one sampler and the masks buffer. No `count` on
|
||||
/// any entry: this needs nothing beyond plain Vulkan 1.0 / GLES
|
||||
/// sampling, unlike the `binding_array` layout it replaced.
|
||||
fn rsc_layout(device: &Device) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: BufferSize::new(size_of::<WindowUniform>() as u64),
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Texture {
|
||||
sample_type: TextureSampleType::Float { filterable: false },
|
||||
view_dimension: TextureViewDimension::D2Array,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Texture {
|
||||
sample_type: TextureSampleType::Float { filterable: false },
|
||||
view_dimension: TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 3,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: BufferSize::new(size_of::<Mask>() as u64),
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("ui shared"),
|
||||
label: Some("ui rsc"),
|
||||
})
|
||||
}
|
||||
|
||||
fn shared_group(
|
||||
/// The main group: rects and glyphs never sample the image slot, so it
|
||||
/// gets a 1x1 null view rather than any live standalone image's.
|
||||
fn rsc_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
window: &Buffer,
|
||||
tex_manager: &GpuTextures,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
@@ -292,85 +381,26 @@ impl UiRenderNode {
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: window.as_entire_binding(),
|
||||
resource: BindingResource::TextureView(tex_manager.array_view()),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: BindingResource::TextureView(tex_manager.null_view()),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: BindingResource::Sampler(tex_manager.sampler()),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: masks.buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui shared"),
|
||||
label: Some("ui rsc"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Layout for a list of one primitive's data. Every size in the ui is
|
||||
/// stated, so "is the buffer big enough for one entry?" is answered when
|
||||
/// the bind group is made; a `None` size is wgpu's to check on every draw.
|
||||
fn data_layout(device: &Device, stride: u64) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: BufferSize::new(stride),
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
label: Some("ui primitive data"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderLayer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
primitives: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListBuffers {
|
||||
fn new(device: &Device) -> Self {
|
||||
Self {
|
||||
instance: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
||||
"instance",
|
||||
),
|
||||
data: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"primitive data",
|
||||
),
|
||||
group: None,
|
||||
bindings: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
primitive: &PrimitivePipeline,
|
||||
list: &InstanceList,
|
||||
) {
|
||||
self.bindings.clear();
|
||||
primitive.render.instance_bindings(list, &mut self.bindings);
|
||||
self.instance.update(device, queue, list.instances());
|
||||
let resized = self.data.update(device, queue, list.data());
|
||||
if list.instances().is_empty() {
|
||||
self.group = None;
|
||||
} else if resized || self.group.is_none() {
|
||||
self.group = Some(device.create_bind_group(&BindGroupDescriptor {
|
||||
layout: &primitive.data_layout,
|
||||
entries: &[BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: self.data.buffer.as_entire_binding(),
|
||||
}],
|
||||
label: Some("ui primitive data"),
|
||||
}));
|
||||
}
|
||||
pub fn view_count(&self) -> usize {
|
||||
self.textures.view_count()
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
use wgpu::*;
|
||||
|
||||
use crate::{GlyphAtlas, UiData};
|
||||
|
||||
use super::{
|
||||
atlas::PAGE,
|
||||
primitive::{ListDraw, PrimitiveRender},
|
||||
texture::{default_sampler, sampled_group, sampled_layout, write_region},
|
||||
};
|
||||
|
||||
/// Draws glyphs from the atlas, which it owns: one array texture bound once
|
||||
/// for a whole list, since every glyph in it reads the same pages.
|
||||
pub struct GlyphRender {
|
||||
pages: GpuPages,
|
||||
layout: BindGroupLayout,
|
||||
sampler: Sampler,
|
||||
}
|
||||
|
||||
impl GlyphRender {
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
let layout = sampled_layout(device, TextureViewDimension::D2Array, "ui atlas");
|
||||
let sampler = default_sampler(device);
|
||||
Self {
|
||||
pages: GpuPages::new(device, queue, &layout, &sampler),
|
||||
layout,
|
||||
sampler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveRender for GlyphRender {
|
||||
fn layout(&self) -> Option<&BindGroupLayout> {
|
||||
Some(&self.layout)
|
||||
}
|
||||
|
||||
fn update(&mut self, ui: &mut UiData) {
|
||||
self.pages
|
||||
.update(&mut ui.text.atlas, &self.layout, &self.sampler);
|
||||
}
|
||||
|
||||
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
||||
pass.set_bind_group(2, self.pages.group(), &[]);
|
||||
pass.draw(0..4, 0..list.instances);
|
||||
}
|
||||
}
|
||||
|
||||
/// The glyph atlas on the GPU: one array texture whose layers are the pages
|
||||
/// `GlyphAtlas` packs.
|
||||
///
|
||||
/// One array rather than a texture per page because a layer index is ordinary
|
||||
/// Vulkan 1.0 / GLES sampling, where a `binding_array` would need
|
||||
/// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack.
|
||||
pub struct GpuPages {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
texture: Texture,
|
||||
group: BindGroup,
|
||||
}
|
||||
|
||||
impl GpuPages {
|
||||
pub fn new(
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
layout: &BindGroupLayout,
|
||||
sampler: &Sampler,
|
||||
) -> Self {
|
||||
let texture = create_array(device, 1);
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
group: atlas_group(device, layout, &texture, sampler),
|
||||
texture,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
if atlas.page_count() > self.texture.depth_or_array_layers() {
|
||||
self.grow(atlas.page_count(), layout, sampler);
|
||||
}
|
||||
for (upload, page) in atlas.uploads() {
|
||||
let dst = TexelCopyTextureInfo {
|
||||
texture: &self.texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: upload.rect.x,
|
||||
y: upload.rect.y,
|
||||
z: upload.layer,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
};
|
||||
write_region(&self.queue, dst, page, upload.rect);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group(&self) -> &BindGroup {
|
||||
&self.group
|
||||
}
|
||||
|
||||
/// Doubles until `needed` fits and copies the old layers across GPU side.
|
||||
/// The new texture stales the group, so that is rebuilt here.
|
||||
fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
let old = self.texture.depth_or_array_layers();
|
||||
let mut layers = old;
|
||||
while layers < needed {
|
||||
layers *= 2;
|
||||
}
|
||||
let texture = create_array(&self.device, layers);
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("atlas grow"),
|
||||
});
|
||||
encoder.copy_texture_to_texture(
|
||||
self.texture.as_image_copy(),
|
||||
texture.as_image_copy(),
|
||||
Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: old,
|
||||
},
|
||||
);
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.group = atlas_group(&self.device, layout, &texture, sampler);
|
||||
self.texture = texture;
|
||||
}
|
||||
}
|
||||
|
||||
fn atlas_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
texture: &Texture,
|
||||
sampler: &Sampler,
|
||||
) -> BindGroup {
|
||||
let view = texture.create_view(&TextureViewDescriptor {
|
||||
dimension: Some(TextureViewDimension::D2Array),
|
||||
..Default::default()
|
||||
});
|
||||
sampled_group(device, layout, &view, sampler, "ui atlas")
|
||||
}
|
||||
|
||||
fn create_array(device: &Device, layers: u32) -> Texture {
|
||||
device.create_texture(&TextureDescriptor {
|
||||
label: Some("glyph atlas"),
|
||||
size: Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: layers,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
+333
-299
@@ -1,247 +1,144 @@
|
||||
use std::{any::TypeId, marker::PhantomData};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::{
|
||||
Color, TextureHandle, UiData, UiRegion, WidgetId,
|
||||
Color, UiRegion, WidgetId,
|
||||
render::{
|
||||
ArrBuf,
|
||||
data::{MaskIdx, PrimitiveInstance},
|
||||
page::GlyphRender,
|
||||
texture::ImageRender,
|
||||
},
|
||||
util::{HashMap, Vec2},
|
||||
util::Vec2,
|
||||
};
|
||||
use bytemuck::Pod;
|
||||
use wgpu::{BindGroupLayout, Device, Queue, RenderPass};
|
||||
use wgpu::*;
|
||||
|
||||
/// One instance of a primitive, laid out as the struct its shader reads.
|
||||
///
|
||||
/// The type carries its own shader, so drawing one is all the wiring it needs:
|
||||
/// its list, free list, buffers and pipeline follow from being registered.
|
||||
pub trait Primitive: Pod + 'static {
|
||||
/// Compiled after `prelude.wgsl`, which states what it declares and what
|
||||
/// it is given.
|
||||
const WGSL: &'static str;
|
||||
|
||||
/// Made once, the first time the renderer sees this primitive. It owns
|
||||
/// whatever the shader samples and records the primitive's own draws; the
|
||||
/// default owns nothing and draws every instance in one call.
|
||||
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let _ = (device, queue);
|
||||
Box::new(Instanced)
|
||||
}
|
||||
}
|
||||
|
||||
/// The renderer's half of a primitive: what it samples, what it uploads, and
|
||||
/// what draws it records.
|
||||
///
|
||||
/// Everything a draw shares -- the pipeline, the window and masks, the list's
|
||||
/// own data and instance buffer -- is set before this is called. What is left
|
||||
/// is what only this primitive knows: its group 2, and how many draws its
|
||||
/// instances are.
|
||||
pub trait PrimitiveRender {
|
||||
/// The layout its shader reads at group 2. `None` for a primitive whose
|
||||
/// shader samples nothing, whose pipeline then has no group 2 at all.
|
||||
fn layout(&self) -> Option<&BindGroupLayout> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Uploads whatever this primitive owns, once a frame, before any draw.
|
||||
fn update(&mut self, ui: &mut UiData) {
|
||||
let _ = ui;
|
||||
}
|
||||
|
||||
/// Keeps what the primitive needs per instance at draw time, read from
|
||||
/// the list's own data. A primitive that binds nothing per instance --
|
||||
/// most of them -- leaves this empty and draws in one call.
|
||||
fn instance_bindings(&self, list: &InstanceList, out: &mut Vec<u32>) {
|
||||
let _ = (list, out);
|
||||
}
|
||||
|
||||
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>);
|
||||
}
|
||||
|
||||
/// What a `PrimitiveRender` draws: this list's instances, and whatever
|
||||
/// `instance_bindings` kept for them.
|
||||
pub struct ListDraw<'a> {
|
||||
pub instances: u32,
|
||||
pub bindings: &'a [u32],
|
||||
}
|
||||
|
||||
/// The default: nothing sampled, every instance in one call.
|
||||
pub struct Instanced;
|
||||
|
||||
impl PrimitiveRender for Instanced {
|
||||
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
||||
pass.draw(0..4, 0..list.instances);
|
||||
}
|
||||
}
|
||||
|
||||
/// Which registered primitive an instance is.
|
||||
pub struct PrimitiveKind<P> {
|
||||
id: u32,
|
||||
_p: PhantomData<fn(P)>,
|
||||
}
|
||||
|
||||
impl<P> PrimitiveKind<P> {
|
||||
fn new(id: u32) -> Self {
|
||||
Self {
|
||||
id,
|
||||
_p: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> Clone for PrimitiveKind<P> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> Copy for PrimitiveKind<P> {}
|
||||
|
||||
/// Every primitive a ui can draw, in the order they were first drawn.
|
||||
#[derive(Default)]
|
||||
pub struct PrimitiveRegistry {
|
||||
kinds: Vec<PrimitiveSource>,
|
||||
ids: HashMap<TypeId, u32>,
|
||||
}
|
||||
|
||||
pub struct PrimitiveSource {
|
||||
pub wgsl: &'static str,
|
||||
pub label: &'static str,
|
||||
/// Size of one instance's entry, stated as the data binding's minimum.
|
||||
pub stride: u64,
|
||||
pub render: fn(&Device, &Queue) -> Box<dyn PrimitiveRender>,
|
||||
}
|
||||
|
||||
impl PrimitiveRegistry {
|
||||
/// Registers `P` if this is the first time it has been drawn.
|
||||
pub fn kind<P: Primitive>(&mut self) -> PrimitiveKind<P> {
|
||||
let Self { kinds, ids } = self;
|
||||
let id = *ids.entry(TypeId::of::<P>()).or_insert_with(|| {
|
||||
kinds.push(PrimitiveSource {
|
||||
wgsl: P::WGSL,
|
||||
label: std::any::type_name::<P>(),
|
||||
stride: size_of::<P>() as u64,
|
||||
render: P::render,
|
||||
});
|
||||
kinds.len() as u32 - 1
|
||||
});
|
||||
PrimitiveKind::new(id)
|
||||
}
|
||||
|
||||
pub fn sources(&self) -> &[PrimitiveSource] {
|
||||
&self.kinds
|
||||
}
|
||||
}
|
||||
|
||||
/// One registered primitive's instances in one layer. Everything per-instance
|
||||
/// rides here, so it stays in step through a `swap_remove`.
|
||||
pub struct InstanceList {
|
||||
pub struct Primitives {
|
||||
instances: Vec<PrimitiveInstance>,
|
||||
/// The widget each instance belongs to, for renumbering its handles.
|
||||
assoc: Vec<WidgetId>,
|
||||
data: PrimitiveData,
|
||||
free: Vec<usize>,
|
||||
/// `stride` bytes of the primitive's own data per instance.
|
||||
data: Vec<u8>,
|
||||
/// From the type the list was made for, so a write is never checked.
|
||||
stride: usize,
|
||||
}
|
||||
|
||||
impl InstanceList {
|
||||
fn new<P: Primitive>() -> Self {
|
||||
Self {
|
||||
instances: Vec::new(),
|
||||
assoc: Vec::new(),
|
||||
free: Vec::new(),
|
||||
data: Vec::new(),
|
||||
stride: size_of::<P>(),
|
||||
}
|
||||
}
|
||||
/// Standalone images, kept apart from `instances` because each one draws
|
||||
/// with its own bind group rather than sharing the layer's one instanced
|
||||
/// draw. `idx` on each `PrimitiveInstance` here is the texture's slot in
|
||||
/// `Textures`/`GpuTextures`, not an index into `data`: a bind group has
|
||||
/// already picked the texture, so there is nothing left to look up
|
||||
/// per-instance and no per-image entry in `data` at all.
|
||||
images: Vec<PrimitiveInstance>,
|
||||
image_assoc: Vec<WidgetId>,
|
||||
image_free: Vec<usize>,
|
||||
|
||||
pub fn instances(&self) -> &[PrimitiveInstance] {
|
||||
&self.instances
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn stride(&self) -> usize {
|
||||
self.stride
|
||||
}
|
||||
|
||||
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> usize {
|
||||
if let Some(i) = self.free.pop() {
|
||||
self.instances[i] = inst;
|
||||
self.assoc[i] = id;
|
||||
self.data[i * self.stride..][..self.stride].copy_from_slice(data);
|
||||
i
|
||||
} else {
|
||||
let i = self.instances.len();
|
||||
self.instances.push(inst);
|
||||
self.assoc.push(id);
|
||||
self.data.extend_from_slice(data);
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
fn free(&mut self, i: usize) -> MaskIdx {
|
||||
self.free.push(i);
|
||||
self.instances[i].mask_idx
|
||||
}
|
||||
|
||||
fn apply_free(&mut self, kind: u32) -> impl Iterator<Item = PrimitiveChange> {
|
||||
self.free.sort_by(|a, b| b.cmp(a));
|
||||
let instances = &mut self.instances;
|
||||
let assoc = &mut self.assoc;
|
||||
let data = &mut self.data;
|
||||
let stride = self.stride;
|
||||
self.free.drain(..).filter_map(move |i| {
|
||||
instances.swap_remove(i);
|
||||
assoc.swap_remove(i);
|
||||
let last = instances.len();
|
||||
data.copy_within(last * stride..(last + 1) * stride, i * stride);
|
||||
data.truncate(last * stride);
|
||||
if i == last {
|
||||
return None;
|
||||
}
|
||||
let id = assoc[i];
|
||||
Some(PrimitiveChange {
|
||||
id,
|
||||
kind,
|
||||
old: last,
|
||||
new: i,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything one layer draws, one list per registered primitive.
|
||||
pub struct LayerDraws {
|
||||
/// `None` until this layer draws that primitive, because only the write
|
||||
/// knows the type the list is for.
|
||||
primitives: Vec<Option<InstanceList>>,
|
||||
pub updated: bool,
|
||||
}
|
||||
|
||||
impl Default for LayerDraws {
|
||||
impl Default for Primitives {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
primitives: Vec::new(),
|
||||
instances: Default::default(),
|
||||
assoc: Default::default(),
|
||||
data: Default::default(),
|
||||
free: Vec::new(),
|
||||
images: Default::default(),
|
||||
image_assoc: Default::default(),
|
||||
image_free: Vec::new(),
|
||||
updated: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayerDraws {
|
||||
/// The `binding` tag `Painter` writes on an image instance. Distinct from any
|
||||
/// `Primitive::BINDING` because images have no `PrimitiveData` entry to key
|
||||
/// one from -- a bind group already selects the texture -- so this only ever
|
||||
/// has to match the shader's `TEXTURE` constant and flag "this instance lives
|
||||
/// in `Primitives::images`, not `Primitives::instances`" to the code below.
|
||||
pub const IMAGE_BINDING: u32 = 1;
|
||||
|
||||
pub trait Primitive: Pod {
|
||||
const BINDING: u32;
|
||||
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>;
|
||||
}
|
||||
|
||||
macro_rules! primitives {
|
||||
($($name:ident: $ty:ty => $binding:expr,)*) => {
|
||||
#[derive(Default)]
|
||||
pub struct PrimitiveData {
|
||||
$(pub(crate) $name: PrimitiveVec<$ty>,)*
|
||||
}
|
||||
|
||||
pub struct PrimitiveBuffers {
|
||||
$($name: ArrBuf<$ty>,)*
|
||||
}
|
||||
|
||||
impl PrimitiveBuffers {
|
||||
pub fn update(&mut self, device: &Device, queue: &Queue, data: &PrimitiveData) {
|
||||
$(self.$name.update(device, queue, &data.$name);)*
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveBuffers {
|
||||
pub const LEN: usize = primitives!(@count $($name)*);
|
||||
/// The group-1 binding number each primitive's storage buffer
|
||||
/// sits at, in declaration order. Not `0..LEN`: a primitive's
|
||||
/// `BINDING` also tags its instances for the shader's dispatch
|
||||
/// switch, and a removed primitive (as `TEXTURE` was, once
|
||||
/// images stopped needing a per-instance storage entry) can
|
||||
/// leave a gap, so the pipeline layout has to ask for these
|
||||
/// exact numbers rather than assuming they are contiguous.
|
||||
pub const BINDINGS: [u32; Self::LEN] = [$(<$ty>::BINDING,)*];
|
||||
pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
|
||||
[
|
||||
$((<$ty>::BINDING, &self.$name.buffer),)*
|
||||
]
|
||||
}
|
||||
pub fn new(device: &Device) -> Self {
|
||||
Self {
|
||||
$($name: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
stringify!($name),
|
||||
),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveData {
|
||||
pub fn clear(&mut self) {
|
||||
$(self.$name.clear();)*
|
||||
}
|
||||
pub fn free(&mut self, binding: u32, idx: usize) {
|
||||
match binding {
|
||||
$(<$ty>::BINDING => self.$name.free(idx),)*
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
unsafe impl bytemuck::Pod for $ty {}
|
||||
unsafe impl bytemuck::Zeroable for $ty {}
|
||||
impl Primitive for $ty {
|
||||
const BINDING: u32 = $binding;
|
||||
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self> {
|
||||
&mut data.$name
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t)+) };
|
||||
(@count $t:tt) => { 1 };
|
||||
}
|
||||
|
||||
pub struct PrimitiveInst<P> {
|
||||
pub id: WidgetId,
|
||||
pub primitive: P,
|
||||
pub region: UiRegion,
|
||||
pub mask_idx: MaskIdx,
|
||||
}
|
||||
|
||||
impl Primitives {
|
||||
pub fn write<P: Primitive>(
|
||||
&mut self,
|
||||
layer: usize,
|
||||
PrimitiveInst {
|
||||
kind,
|
||||
id,
|
||||
primitive,
|
||||
region,
|
||||
@@ -249,67 +146,151 @@ impl LayerDraws {
|
||||
}: PrimitiveInst<P>,
|
||||
) -> PrimitiveHandle {
|
||||
self.updated = true;
|
||||
// Grown on first use rather than sized from the registry, which a
|
||||
// layer cannot see.
|
||||
if self.primitives.len() <= kind.id as usize {
|
||||
self.primitives.resize_with(kind.id as usize + 1, || None);
|
||||
}
|
||||
let inst_idx = self.primitives[kind.id as usize]
|
||||
.get_or_insert_with(InstanceList::new::<P>)
|
||||
.push(
|
||||
id,
|
||||
PrimitiveInstance { region, mask_idx },
|
||||
bytemuck::bytes_of(&primitive),
|
||||
);
|
||||
let vec = P::vec(&mut self.data);
|
||||
let i = vec.add(primitive);
|
||||
let inst = PrimitiveInstance {
|
||||
region,
|
||||
idx: i as u32,
|
||||
mask_idx,
|
||||
binding: P::BINDING,
|
||||
};
|
||||
let inst_i = if let Some(i) = self.free.pop() {
|
||||
self.instances[i] = inst;
|
||||
self.assoc[i] = id;
|
||||
i
|
||||
} else {
|
||||
let i = self.instances.len();
|
||||
self.instances.push(inst);
|
||||
self.assoc.push(id);
|
||||
i
|
||||
};
|
||||
PrimitiveHandle::new::<P>(layer, inst_i, i)
|
||||
}
|
||||
|
||||
/// Writes an image instance directly -- there is no `Primitive` impl for
|
||||
/// it to go through `write`, since it has nowhere in `PrimitiveData` to
|
||||
/// put a per-instance entry. `texture_idx` is the slot the bind group at
|
||||
/// draw time is chosen from, carried in the otherwise-unused `idx` field.
|
||||
pub fn write_image(
|
||||
&mut self,
|
||||
layer: usize,
|
||||
id: WidgetId,
|
||||
texture_idx: u32,
|
||||
region: UiRegion,
|
||||
mask_idx: MaskIdx,
|
||||
) -> PrimitiveHandle {
|
||||
self.updated = true;
|
||||
let inst = PrimitiveInstance {
|
||||
region,
|
||||
idx: texture_idx,
|
||||
mask_idx,
|
||||
binding: IMAGE_BINDING,
|
||||
};
|
||||
let inst_i = if let Some(i) = self.image_free.pop() {
|
||||
self.images[i] = inst;
|
||||
self.image_assoc[i] = id;
|
||||
i
|
||||
} else {
|
||||
let i = self.images.len();
|
||||
self.images.push(inst);
|
||||
self.image_assoc.push(id);
|
||||
i
|
||||
};
|
||||
PrimitiveHandle {
|
||||
layer,
|
||||
kind: kind.id,
|
||||
inst_idx,
|
||||
inst_idx: inst_i,
|
||||
data_idx: 0,
|
||||
binding: IMAGE_BINDING,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primitives(&self) -> &[Option<InstanceList>] {
|
||||
&self.primitives
|
||||
pub fn image_instances(&self) -> &Vec<PrimitiveInstance> {
|
||||
&self.images
|
||||
}
|
||||
|
||||
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
|
||||
self.primitives
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?)))
|
||||
.flat_map(|(kind, list)| list.apply_free(kind))
|
||||
/// returns (old index, new index) for both lists this layer keeps --
|
||||
/// `PrimitiveChange::is_image` says which, since the two have separate
|
||||
/// index spaces and `old`/`new` alone would collide between them.
|
||||
///
|
||||
/// Both lists free with `swap_remove`, so a layer's draw order was
|
||||
/// already undefined before images existed: nothing here may assume one
|
||||
/// primitive stays adjacent to another once anything in the layer has
|
||||
/// been freed.
|
||||
pub fn apply_free(&mut self) -> Vec<PrimitiveChange> {
|
||||
let mut changes =
|
||||
Self::apply_free_list(&mut self.free, &mut self.instances, &mut self.assoc, false);
|
||||
changes.extend(Self::apply_free_list(
|
||||
&mut self.image_free,
|
||||
&mut self.images,
|
||||
&mut self.image_assoc,
|
||||
true,
|
||||
));
|
||||
changes
|
||||
}
|
||||
|
||||
fn apply_free_list(
|
||||
free: &mut Vec<usize>,
|
||||
instances: &mut Vec<PrimitiveInstance>,
|
||||
assoc: &mut Vec<WidgetId>,
|
||||
is_image: bool,
|
||||
) -> Vec<PrimitiveChange> {
|
||||
free.sort_by(|a, b| b.cmp(a));
|
||||
free.drain(..)
|
||||
.filter_map(|i| {
|
||||
instances.swap_remove(i);
|
||||
assoc.swap_remove(i);
|
||||
if i == instances.len() {
|
||||
return None;
|
||||
}
|
||||
let id = assoc[i];
|
||||
let old = instances.len();
|
||||
Some(PrimitiveChange {
|
||||
id,
|
||||
is_image,
|
||||
old,
|
||||
new: i,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
||||
self.updated = true;
|
||||
self.list(h).free(h.inst_idx)
|
||||
if h.binding == IMAGE_BINDING {
|
||||
self.image_free.push(h.inst_idx);
|
||||
self.images[h.inst_idx].mask_idx
|
||||
} else {
|
||||
self.data.free(h.binding, h.data_idx);
|
||||
self.free.push(h.inst_idx);
|
||||
self.instances[h.inst_idx].mask_idx
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &PrimitiveData {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
|
||||
&self.instances
|
||||
}
|
||||
|
||||
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
|
||||
self.updated = true;
|
||||
&mut self.list(h).instances[h.inst_idx].region
|
||||
if h.binding == IMAGE_BINDING {
|
||||
&mut self.images[h.inst_idx].region
|
||||
} else {
|
||||
&mut self.instances[h.inst_idx].region
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle is only ever made by `write`, which is what created the list.
|
||||
fn list(&mut self, h: &PrimitiveHandle) -> &mut InstanceList {
|
||||
self.primitives[h.kind as usize]
|
||||
.as_mut()
|
||||
.expect("handle names a primitive this layer never drew")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrimitiveInst<P> {
|
||||
pub kind: PrimitiveKind<P>,
|
||||
pub id: WidgetId,
|
||||
pub primitive: P,
|
||||
pub region: UiRegion,
|
||||
pub mask_idx: MaskIdx,
|
||||
}
|
||||
|
||||
pub struct PrimitiveChange {
|
||||
pub id: WidgetId,
|
||||
/// Which registered primitive's list moved, since they index separately.
|
||||
pub kind: u32,
|
||||
/// Which of `Primitives::instances`/`Primitives::images` this change
|
||||
/// belongs to -- their `old`/`new` indices are independent, so a
|
||||
/// consumer matching only on `(layer, inst_idx)` could apply an image's
|
||||
/// renumbering to a rect's handle that happens to share the same index.
|
||||
pub is_image: bool,
|
||||
pub old: usize,
|
||||
pub new: usize,
|
||||
}
|
||||
@@ -317,12 +298,29 @@ pub struct PrimitiveChange {
|
||||
#[derive(Debug)]
|
||||
pub struct PrimitiveHandle {
|
||||
pub layer: usize,
|
||||
pub kind: u32,
|
||||
pub inst_idx: usize,
|
||||
pub data_idx: usize,
|
||||
pub binding: u32,
|
||||
}
|
||||
|
||||
impl PrimitiveHandle {
|
||||
fn new<P: Primitive>(layer: usize, inst_idx: usize, data_idx: usize) -> Self {
|
||||
Self {
|
||||
layer,
|
||||
inst_idx,
|
||||
data_idx,
|
||||
binding: P::BINDING,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
primitives!(
|
||||
rects: RectPrimitive => 0,
|
||||
glyphs: GlyphPrimitive => 2,
|
||||
);
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct RectPrimitive {
|
||||
pub color: Color<u8>,
|
||||
pub radius: f32,
|
||||
@@ -330,10 +328,6 @@ pub struct RectPrimitive {
|
||||
pub inner_radius: f32,
|
||||
}
|
||||
|
||||
impl Primitive for RectPrimitive {
|
||||
const WGSL: &'static str = include_str!("shader/rect.wgsl");
|
||||
}
|
||||
|
||||
impl RectPrimitive {
|
||||
pub fn color(color: Color<u8>) -> Self {
|
||||
Self {
|
||||
@@ -345,51 +339,91 @@ impl RectPrimitive {
|
||||
}
|
||||
}
|
||||
|
||||
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
|
||||
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
|
||||
#[repr(C, align(8))]
|
||||
/// One glyph, drawn as a sub-rectangle of the glyph atlas array.
|
||||
///
|
||||
/// `color` is the text colour and is multiplied by the atlas's alpha for an
|
||||
/// ordinary mask glyph; a colour glyph (emoji) carries its own colour and
|
||||
/// takes the atlas texel unchanged, which is what `GlyphEntry::IS_COLORED`
|
||||
/// selects.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct GlyphPrimitive {
|
||||
pub uv_min: Vec2,
|
||||
pub uv_max: Vec2,
|
||||
/// Which atlas array layer this glyph is on.
|
||||
/// Layer of the shared atlas array texture this glyph's page occupies --
|
||||
/// not a bind-group or view index, since a page never gets one of its own.
|
||||
pub layer: u32,
|
||||
pub color: Color<u8>,
|
||||
pub flags: u32,
|
||||
/// Pads this struct's Rust size to match WGSL's storage-buffer layout for
|
||||
/// `GlyphInfo`: two `vec2<f32>` members give the struct an 8-byte
|
||||
/// alignment, which rounds the WGSL size up to 32 bytes even though the
|
||||
/// fields above only total 28. `bytemuck` does not check this for us.
|
||||
_pad: u32,
|
||||
}
|
||||
|
||||
// Manual rather than derived: the align(8) leaves four bytes of padding, which
|
||||
// is how WGSL lays the struct out.
|
||||
unsafe impl bytemuck::Pod for GlyphPrimitive {}
|
||||
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
|
||||
impl Primitive for GlyphPrimitive {
|
||||
const WGSL: &'static str = include_str!("shader/glyph.wgsl");
|
||||
|
||||
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
|
||||
Box::new(GlyphRender::new(device, queue))
|
||||
}
|
||||
}
|
||||
|
||||
/// One drawn image. Its shader reads nothing per instance; the slot names the
|
||||
/// texture to bind for it.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct TexturePrimitive {
|
||||
pub slot: u32,
|
||||
}
|
||||
|
||||
impl Primitive for TexturePrimitive {
|
||||
const WGSL: &'static str = include_str!("shader/texture.wgsl");
|
||||
|
||||
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
|
||||
Box::new(ImageRender::new(device, queue))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TextureHandle> for TexturePrimitive {
|
||||
fn from(handle: &TextureHandle) -> Self {
|
||||
impl GlyphPrimitive {
|
||||
/// The only constructor, since `_pad` is private: callers outside this
|
||||
/// module cannot write the struct literal.
|
||||
pub fn new(uv_min: Vec2, uv_max: Vec2, layer: u32, color: Color<u8>, flags: u32) -> Self {
|
||||
Self {
|
||||
slot: handle.slot(),
|
||||
uv_min,
|
||||
uv_max,
|
||||
layer,
|
||||
color,
|
||||
flags,
|
||||
_pad: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrimitiveVec<T> {
|
||||
vec: Vec<T>,
|
||||
free: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<T> PrimitiveVec<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
vec: Vec::new(),
|
||||
free: Vec::new(),
|
||||
}
|
||||
}
|
||||
pub fn add(&mut self, t: T) -> usize {
|
||||
if let Some(i) = self.free.pop() {
|
||||
self.vec[i] = t;
|
||||
i
|
||||
} else {
|
||||
let i = self.vec.len();
|
||||
self.vec.push(t);
|
||||
i
|
||||
}
|
||||
}
|
||||
pub fn free(&mut self, i: usize) {
|
||||
self.free.push(i);
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.free.clear();
|
||||
self.vec.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for PrimitiveVec<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for PrimitiveVec<T> {
|
||||
type Target = Vec<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.vec
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for PrimitiveVec<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.vec
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
const RECT: u32 = 0u;
|
||||
// TEXTURE has no entry in group 1: a standalone image draws with its own
|
||||
// bind group (see UiRenderNode::draw), so there is nothing per-instance left
|
||||
// to look up here -- the bind group already picked the texture.
|
||||
const TEXTURE: u32 = 1u;
|
||||
const GLYPH: u32 = 2u;
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> window: WindowUniform;
|
||||
@group(1) @binding(RECT)
|
||||
var<storage> rects: array<Rect>;
|
||||
@group(1) @binding(GLYPH)
|
||||
var<storage> glyphs: array<GlyphInfo>;
|
||||
|
||||
struct Rect {
|
||||
color: u32,
|
||||
radius: f32,
|
||||
thickness: f32,
|
||||
inner_radius: f32,
|
||||
}
|
||||
|
||||
struct GlyphInfo {
|
||||
uv_min: vec2<f32>,
|
||||
uv_max: vec2<f32>,
|
||||
// Layer of the shared atlas array texture, not a view or bind-group
|
||||
// index -- a page never gets its own bind group.
|
||||
layer: u32,
|
||||
color: u32,
|
||||
flags: u32,
|
||||
}
|
||||
|
||||
struct Mask {
|
||||
x: UiSpan,
|
||||
y: UiSpan,
|
||||
}
|
||||
|
||||
struct UiSpan {
|
||||
start: UiScalar,
|
||||
end: UiScalar,
|
||||
}
|
||||
|
||||
struct UiScalar {
|
||||
rel: f32,
|
||||
abs: f32,
|
||||
}
|
||||
|
||||
struct UiVec2 {
|
||||
rel: vec2<f32>,
|
||||
abs: vec2<f32>,
|
||||
}
|
||||
|
||||
// The shared glyph atlas: every page is one layer. Growing it recreates this
|
||||
// texture with headroom and copies the old layers across -- see
|
||||
// GpuTextures::grow_array -- rather than the binding_array<texture_2d<f32>>
|
||||
// this replaced, which needed VK_EXT_descriptor_indexing and does not survive
|
||||
// a real share of Android GPUs.
|
||||
@group(2) @binding(0)
|
||||
var atlas: texture_2d_array<f32>;
|
||||
// One standalone image's texture. The main draw (rects and glyphs) binds a
|
||||
// 1x1 null texture here, since neither samples it; each image draw call
|
||||
// binds its own -- see UiRenderNode::draw.
|
||||
@group(2) @binding(1)
|
||||
var image_texture: texture_2d<f32>;
|
||||
@group(2) @binding(2)
|
||||
var samp: sampler;
|
||||
@group(2) @binding(3)
|
||||
var<storage> masks: array<Mask>;
|
||||
|
||||
struct WindowUniform {
|
||||
dim: vec2<f32>,
|
||||
};
|
||||
|
||||
struct InstanceInput {
|
||||
@location(0) x_start: vec2<f32>,
|
||||
@location(1) x_end: vec2<f32>,
|
||||
@location(2) y_start: vec2<f32>,
|
||||
@location(3) y_end: vec2<f32>,
|
||||
@location(4) binding: u32,
|
||||
@location(5) idx: u32,
|
||||
@location(6) mask_idx: u32,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@location(0) top_left: vec2<f32>,
|
||||
@location(1) bot_right: vec2<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) binding: u32,
|
||||
@location(4) idx: u32,
|
||||
@location(5) mask_idx: u32,
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
};
|
||||
|
||||
struct Region {
|
||||
pos: vec2<f32>,
|
||||
uv: vec2<f32>,
|
||||
top_left: vec2<f32>,
|
||||
bot_right: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@builtin(vertex_index) vi: u32,
|
||||
in: InstanceInput,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
|
||||
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
||||
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
||||
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
||||
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
|
||||
|
||||
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
||||
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
||||
let size = bot_right - top_left;
|
||||
|
||||
let uv = vec2<f32>(
|
||||
f32(vi % 2u),
|
||||
f32(vi / 2u)
|
||||
);
|
||||
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
|
||||
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
||||
out.uv = uv;
|
||||
out.binding = in.binding;
|
||||
out.idx = in.idx;
|
||||
out.top_left = top_left;
|
||||
out.bot_right = bot_right;
|
||||
out.mask_idx = in.mask_idx;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(
|
||||
in: VertexOutput
|
||||
) -> @location(0) vec4<f32> {
|
||||
let pos = in.clip_position.xy;
|
||||
let region = Region(pos, in.uv, in.top_left, in.bot_right);
|
||||
let i = in.idx;
|
||||
var color: vec4<f32>;
|
||||
switch in.binding {
|
||||
case RECT: {
|
||||
color = draw_rounded_rect(region, rects[i]);
|
||||
}
|
||||
case TEXTURE: {
|
||||
color = draw_texture(region);
|
||||
}
|
||||
case GLYPH: {
|
||||
color = draw_glyph(region, glyphs[i]);
|
||||
}
|
||||
default: {
|
||||
color = vec4(1.0, 0.0, 1.0, 1.0);
|
||||
}
|
||||
}
|
||||
if in.mask_idx != 4294967295u {
|
||||
let mask = masks[in.mask_idx];
|
||||
let tl = UiVec2(vec2(mask.x.start.rel, mask.y.start.rel), vec2(mask.x.start.abs, mask.y.start.abs));
|
||||
let br = UiVec2(vec2(mask.x.end.rel, mask.y.end.rel), vec2(mask.x.end.abs, mask.y.end.abs));
|
||||
|
||||
let top_left = floor(tl.rel * window.dim) + floor(tl.abs);
|
||||
let bot_right = floor(br.rel * window.dim) + floor(br.abs);
|
||||
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
||||
color *= 0.0;
|
||||
}
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
fn draw_texture(region: Region) -> vec4<f32> {
|
||||
return textureSample(image_texture, samp, region.uv);
|
||||
}
|
||||
|
||||
fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
|
||||
let uv = mix(g.uv_min, g.uv_max, region.uv);
|
||||
let texel = textureSample(atlas, samp, uv, i32(g.layer));
|
||||
if (g.flags & 1u) != 0u {
|
||||
return texel;
|
||||
}
|
||||
var color = unpack4x8unorm(g.color);
|
||||
color.a *= texel.a;
|
||||
return color;
|
||||
}
|
||||
|
||||
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
|
||||
var color = unpack4x8unorm(rect.color);
|
||||
|
||||
let edge = 0.5;
|
||||
|
||||
let size = region.bot_right - region.top_left;
|
||||
let corner = size / 2.0;
|
||||
let center = region.top_left + corner;
|
||||
|
||||
let dist = distance_from_rect(region.pos, center, corner, rect.radius);
|
||||
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
|
||||
|
||||
if rect.thickness > 0.0 {
|
||||
let dist2 = distance_from_rect(region.pos, center, corner - rect.thickness, rect.inner_radius);
|
||||
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
|
||||
// vec from center to pixel
|
||||
let p = pixel_pos - rect_center;
|
||||
// vec from inner rect corner to pixel
|
||||
let q = abs(p) - (rect_corner - radius);
|
||||
return length(max(q, vec2(0.0))) - radius;
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Matches `GlyphEntry::IS_COLORED`.
|
||||
const COLORED: u32 = 1u;
|
||||
|
||||
// The glyph atlas, whose array layers are its pages.
|
||||
@group(2) @binding(0)
|
||||
var atlas: texture_2d_array<f32>;
|
||||
@group(2) @binding(1)
|
||||
var samp: sampler;
|
||||
|
||||
struct GlyphInfo {
|
||||
uv_min: vec2<f32>,
|
||||
uv_max: vec2<f32>,
|
||||
// Which layer of the atlas array this glyph's page is.
|
||||
layer: u32,
|
||||
color: u32,
|
||||
flags: u32,
|
||||
}
|
||||
|
||||
@group(1) @binding(0)
|
||||
var<storage> glyphs: array<GlyphInfo>;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let g = glyphs[in.idx];
|
||||
let uv = mix(g.uv_min, g.uv_max, in.uv);
|
||||
let texel = textureSample(atlas, samp, uv, i32(g.layer));
|
||||
if (g.flags & COLORED) != 0u {
|
||||
return masked(in, texel);
|
||||
}
|
||||
var color = unpack4x8unorm(g.color);
|
||||
color.a *= texel.a;
|
||||
return masked(in, color);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// Prepended to every primitive's shader, which declares its own instance data
|
||||
// as `var<storage> <name>: array<T>` at group 1 binding 0, and an `fs_main`
|
||||
// shading one instance of it. What it samples, if anything, is bound at group
|
||||
// 2: the texture at binding 0 and the sampler at binding 1.
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> window: WindowUniform;
|
||||
@group(0) @binding(1)
|
||||
var<storage> masks: array<Mask>;
|
||||
|
||||
struct WindowUniform {
|
||||
dim: vec2<f32>,
|
||||
};
|
||||
|
||||
struct Mask {
|
||||
x: UiSpan,
|
||||
y: UiSpan,
|
||||
}
|
||||
|
||||
struct UiSpan {
|
||||
start: UiScalar,
|
||||
end: UiScalar,
|
||||
}
|
||||
|
||||
struct UiScalar {
|
||||
rel: f32,
|
||||
abs: f32,
|
||||
}
|
||||
|
||||
struct InstanceInput {
|
||||
@location(0) x_start: vec2<f32>,
|
||||
@location(1) x_end: vec2<f32>,
|
||||
@location(2) y_start: vec2<f32>,
|
||||
@location(3) y_end: vec2<f32>,
|
||||
@location(4) mask_idx: u32,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@location(0) top_left: vec2<f32>,
|
||||
@location(1) bot_right: vec2<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) @interpolate(flat) mask_idx: u32,
|
||||
@location(4) @interpolate(flat) idx: u32,
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@builtin(vertex_index) vi: u32,
|
||||
@builtin(instance_index) ii: u32,
|
||||
in: InstanceInput,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
|
||||
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
||||
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
||||
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
||||
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
|
||||
|
||||
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
||||
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
||||
let size = bot_right - top_left;
|
||||
|
||||
let uv = vec2<f32>(
|
||||
f32(vi % 2u),
|
||||
f32(vi / 2u)
|
||||
);
|
||||
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
|
||||
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
||||
out.uv = uv;
|
||||
out.top_left = top_left;
|
||||
out.bot_right = bot_right;
|
||||
out.mask_idx = in.mask_idx;
|
||||
out.idx = ii;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
||||
if in.mask_idx == 4294967295u {
|
||||
return color;
|
||||
}
|
||||
let mask = masks[in.mask_idx];
|
||||
let tl = vec2(mask.x.start.rel, mask.y.start.rel);
|
||||
let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
|
||||
let br = vec2(mask.x.end.rel, mask.y.end.rel);
|
||||
let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
|
||||
|
||||
let top_left = floor(tl * window.dim) + floor(tl_abs);
|
||||
let bot_right = floor(br * window.dim) + floor(br_abs);
|
||||
let pos = in.clip_position.xy;
|
||||
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
||||
return color * 0.0;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
struct Rect {
|
||||
color: u32,
|
||||
radius: f32,
|
||||
thickness: f32,
|
||||
inner_radius: f32,
|
||||
}
|
||||
|
||||
@group(1) @binding(0)
|
||||
var<storage> rects: array<Rect>;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let rect = rects[in.idx];
|
||||
var color = unpack4x8unorm(rect.color);
|
||||
|
||||
let edge = 0.5;
|
||||
let size = in.bot_right - in.top_left;
|
||||
let corner = size / 2.0;
|
||||
let center = in.top_left + corner;
|
||||
let pos = in.clip_position.xy;
|
||||
|
||||
let dist = distance_from_rect(pos, center, corner, rect.radius);
|
||||
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
|
||||
|
||||
if rect.thickness > 0.0 {
|
||||
let dist2 = distance_from_rect(pos, center, corner - rect.thickness, rect.inner_radius);
|
||||
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
|
||||
}
|
||||
|
||||
return masked(in, color);
|
||||
}
|
||||
|
||||
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
|
||||
// vec from center to pixel
|
||||
let p = pixel_pos - rect_center;
|
||||
// vec from inner rect corner to pixel
|
||||
let q = abs(p) - (rect_corner - radius);
|
||||
return length(max(q, vec2(0.0))) - radius;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// The image this instance draws, bound for it alone.
|
||||
@group(2) @binding(0)
|
||||
var image: texture_2d<f32>;
|
||||
@group(2) @binding(1)
|
||||
var samp: sampler;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return masked(in, textureSample(image, samp, in.uv));
|
||||
}
|
||||
+398
-182
@@ -1,112 +1,289 @@
|
||||
use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage};
|
||||
use image::{DynamicImage, EncodableLayout, GenericImageView};
|
||||
use wgpu::{util::DeviceExt, *};
|
||||
|
||||
use crate::{
|
||||
PatchRect, TextureUpdate, Textures, UiData,
|
||||
render::{
|
||||
TexturePrimitive,
|
||||
primitive::{ListDraw, PrimitiveRender},
|
||||
},
|
||||
};
|
||||
use crate::{Mask, PatchRect, TextureKind, TextureUpdate, Textures, render::util::ArrBuf};
|
||||
|
||||
/// Draws standalone images, which it owns. Each is its own texture, so each
|
||||
/// instance binds its own and is a draw of its own.
|
||||
pub struct ImageRender {
|
||||
textures: GpuTextures,
|
||||
layout: BindGroupLayout,
|
||||
sampler: Sampler,
|
||||
}
|
||||
use super::atlas::PAGE;
|
||||
|
||||
impl ImageRender {
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
Self {
|
||||
textures: GpuTextures::new(device, queue),
|
||||
layout: sampled_layout(device, TextureViewDimension::D2, "ui image"),
|
||||
sampler: default_sampler(device),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveRender for ImageRender {
|
||||
fn layout(&self) -> Option<&BindGroupLayout> {
|
||||
Some(&self.layout)
|
||||
}
|
||||
|
||||
fn update(&mut self, ui: &mut UiData) {
|
||||
self.textures
|
||||
.update(&mut ui.textures, &self.layout, &self.sampler);
|
||||
}
|
||||
|
||||
fn instance_bindings(&self, list: &super::InstanceList, out: &mut Vec<u32>) {
|
||||
let slots = list
|
||||
.data()
|
||||
.chunks_exact(list.stride())
|
||||
.map(|data| bytemuck::pod_read_unaligned::<TexturePrimitive>(data).slot);
|
||||
out.extend(slots);
|
||||
}
|
||||
|
||||
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
||||
for (i, &slot) in list.bindings.iter().enumerate() {
|
||||
let Some(image) = self.textures.group(slot) else {
|
||||
continue;
|
||||
};
|
||||
pass.set_bind_group(2, image, &[]);
|
||||
pass.draw(0..4, i as u32..i as u32 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The standalone images a ui draws, each its own texture and bind group --
|
||||
/// unlike the glyph atlas in `super::page`, which is one array they share.
|
||||
pub struct GpuTextures {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
slots: Vec<Option<ImageGpu>>,
|
||||
/// What one texture slot is, GPU-side. Parallel to `Textures`' own slot
|
||||
/// numbering (`TextureKind`'s `Image`/`Page`), so a slot's index means the
|
||||
/// same thing on both sides without a second map to keep in sync.
|
||||
enum Slot {
|
||||
/// A slot that was freed, or pushed and freed within the same batch
|
||||
/// before ever reaching here.
|
||||
Empty,
|
||||
Image(ImageGpu),
|
||||
/// The array layer a page occupies. Pages are never freed (see
|
||||
/// `Textures::free`), so this is the only variant that outlives a `Free`.
|
||||
Page(u32),
|
||||
}
|
||||
|
||||
struct ImageGpu {
|
||||
/// Kept for `patch`, which needs the texture rather than the view.
|
||||
texture: Texture,
|
||||
group: BindGroup,
|
||||
/// Kept because a masks or atlas-array rebuild has to build a new bind
|
||||
/// group from it. The `Texture` it came from is not kept: a `TextureView`
|
||||
/// holds its own reference to that, so the image survives without one.
|
||||
view: TextureView,
|
||||
bind_group: BindGroup,
|
||||
}
|
||||
|
||||
/// Owns the two kinds of texture iris draws:
|
||||
///
|
||||
/// - **The glyph atlas**, one `texture_2d_array` whose layers are pages
|
||||
/// (`Slot::Page`), grown by recreating the array with headroom and
|
||||
/// `copy_texture_to_texture`-ing the old layers across. No feature beyond
|
||||
/// Vulkan 1.0/GLES sampling is needed for this -- a layer index is an
|
||||
/// ordinary sampling operand.
|
||||
/// - **Standalone images** (`Slot::Image`), each its own `Texture` and
|
||||
/// `BindGroup`, drawn one `draw()` call at a time with that bind group
|
||||
/// bound -- see `UiRenderNode::draw`.
|
||||
///
|
||||
/// This replaced one giant `binding_array<texture_2d<f32>>`, which needed
|
||||
/// `VK_EXT_descriptor_indexing` -- an extension a real share of Android GPUs
|
||||
/// lack, so the old shape did not run there at all.
|
||||
pub struct GpuTextures {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
|
||||
slots: Vec<Slot>,
|
||||
|
||||
array_texture: Texture,
|
||||
array_view: TextureView,
|
||||
array_capacity: u32,
|
||||
/// Layers actually written. Only grows -- see `Slot::Page`.
|
||||
page_count: u32,
|
||||
|
||||
sampler: Sampler,
|
||||
/// Bound in the image slot of the main draw's bind group, which has
|
||||
/// nothing of its own to put there: rects and glyphs never sample it,
|
||||
/// but the layout requires something bound regardless.
|
||||
null_view: TextureView,
|
||||
}
|
||||
|
||||
impl GpuTextures {
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
slots: Vec::new(),
|
||||
/// Applies queued `Textures` updates, then reports whether the *main*
|
||||
/// bind group (the one rects and glyphs draw with) needs rebuilding --
|
||||
/// true when the atlas array was recreated (its view identity changed)
|
||||
/// or the masks buffer was, since both are bound there. Pushing or
|
||||
/// freeing a standalone image never touches that group: it built or drops
|
||||
/// its own.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
textures: &mut Textures,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
masks_resized: bool,
|
||||
) -> bool {
|
||||
let mut rebuild_main = masks_resized;
|
||||
if masks_resized {
|
||||
// The masks buffer just moved, so every bind group holding a
|
||||
// reference to it -- one per live standalone image -- is stale.
|
||||
self.rebuild_image_bind_groups(rsc_layout, masks);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
for update in textures.updates() {
|
||||
match update {
|
||||
TextureUpdate::Push(image) => {
|
||||
let image = self.create(image, layout, sampler);
|
||||
self.slots.push(Some(image));
|
||||
TextureUpdate::Push(kind, image) => {
|
||||
rebuild_main |= self.push(kind, image, rsc_layout, masks);
|
||||
}
|
||||
TextureUpdate::Set(i, image) => {
|
||||
let image = self.create(image, layout, sampler);
|
||||
self.slots[i as usize] = Some(image);
|
||||
TextureUpdate::Set(kind, i, image) => {
|
||||
rebuild_main |= self.set(kind, i, image, rsc_layout, masks);
|
||||
}
|
||||
// A patch changes texture contents, not which layer or bind
|
||||
// group exists, so it never asks for a rebuild -- rebuilding
|
||||
// per glyph is exactly the cost this exists to avoid.
|
||||
TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
|
||||
TextureUpdate::PushFree => self.slots.push(None),
|
||||
TextureUpdate::SetFree => {}
|
||||
TextureUpdate::Free(i) => self.slots[i as usize] = None,
|
||||
TextureUpdate::Free(i) => self.free(i),
|
||||
TextureUpdate::PushFree(_kind) => self.slots.push(Slot::Empty),
|
||||
}
|
||||
}
|
||||
rebuild_main
|
||||
}
|
||||
|
||||
fn push(
|
||||
&mut self,
|
||||
kind: TextureKind,
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> bool {
|
||||
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks);
|
||||
self.slots.push(slot);
|
||||
rebuilt
|
||||
}
|
||||
|
||||
fn set(
|
||||
&mut self,
|
||||
kind: TextureKind,
|
||||
i: u32,
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> bool {
|
||||
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks);
|
||||
self.slots[i as usize] = slot;
|
||||
rebuilt
|
||||
}
|
||||
|
||||
fn make_slot(
|
||||
&mut self,
|
||||
kind: TextureKind,
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> (Slot, bool) {
|
||||
match kind {
|
||||
TextureKind::Image => {
|
||||
let gpu = self.create_image(image, rsc_layout, masks);
|
||||
(Slot::Image(gpu), false)
|
||||
}
|
||||
TextureKind::Page { layer } => {
|
||||
let mut rebuilt = false;
|
||||
if layer >= self.array_capacity {
|
||||
self.grow_array(rsc_layout, masks);
|
||||
rebuilt = true;
|
||||
}
|
||||
self.write_full_layer(layer, image);
|
||||
self.page_count = self.page_count.max(layer + 1);
|
||||
(Slot::Page(layer), rebuilt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group(&self, slot: u32) -> Option<&BindGroup> {
|
||||
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
|
||||
fn free(&mut self, i: u32) {
|
||||
if let Some(slot) = self.slots.get_mut(i as usize) {
|
||||
*slot = Slot::Empty;
|
||||
}
|
||||
// A page's layer is not reclaimed here either -- see `Slot::Page`.
|
||||
}
|
||||
|
||||
fn create(
|
||||
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
||||
let Some(&Slot::Page(layer)) = self.slots.get(i as usize) else {
|
||||
return;
|
||||
};
|
||||
if rect.width == 0 || rect.height == 0 {
|
||||
return;
|
||||
}
|
||||
// `write_texture` requires tightly packed rows, unlike the atlas image.
|
||||
let sub = image
|
||||
.view(rect.x, rect.y, rect.width, rect.height)
|
||||
.to_image();
|
||||
self.queue.write_texture(
|
||||
TexelCopyTextureInfo {
|
||||
texture: &self.array_texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
z: layer,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
sub.as_bytes(),
|
||||
TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(rect.width * 4),
|
||||
rows_per_image: Some(rect.height),
|
||||
},
|
||||
Extent3d {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn write_full_layer(&self, layer: u32, image: &DynamicImage) {
|
||||
// Every page is created as exactly PAGE x PAGE (`GlyphAtlas::allocate`),
|
||||
// so this is always a whole-layer write, never a crop.
|
||||
let rgba = image.to_rgba8();
|
||||
self.queue.write_texture(
|
||||
TexelCopyTextureInfo {
|
||||
texture: &self.array_texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: layer,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
rgba.as_bytes(),
|
||||
TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(PAGE * 4),
|
||||
rows_per_image: Some(PAGE),
|
||||
},
|
||||
Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Doubles the array's layer capacity (headroom, so this is rare) and
|
||||
/// copies the old layers across GPU-side -- no readback. Recreates the
|
||||
/// array's view, which invalidates every bind group that referenced it,
|
||||
/// so this also rebuilds all of them before returning.
|
||||
fn grow_array(&mut self, rsc_layout: &BindGroupLayout, masks: &ArrBuf<Mask>) {
|
||||
let new_capacity = self.array_capacity * 2;
|
||||
let new_texture = Self::create_array_texture(&self.device, new_capacity);
|
||||
if self.page_count > 0 {
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("atlas array grow"),
|
||||
});
|
||||
encoder.copy_texture_to_texture(
|
||||
TexelCopyTextureInfo {
|
||||
texture: &self.array_texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d::ZERO,
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
TexelCopyTextureInfo {
|
||||
texture: &new_texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d::ZERO,
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: self.page_count,
|
||||
},
|
||||
);
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
}
|
||||
self.array_texture = new_texture;
|
||||
self.array_view = self.array_texture.create_view(&TextureViewDescriptor {
|
||||
dimension: Some(TextureViewDimension::D2Array),
|
||||
..Default::default()
|
||||
});
|
||||
self.array_capacity = new_capacity;
|
||||
self.rebuild_image_bind_groups(rsc_layout, masks);
|
||||
}
|
||||
|
||||
fn rebuild_image_bind_groups(&mut self, rsc_layout: &BindGroupLayout, masks: &ArrBuf<Mask>) {
|
||||
for slot in &mut self.slots {
|
||||
if let Slot::Image(gpu) = slot {
|
||||
gpu.bind_group = Self::make_image_bind_group(
|
||||
&self.device,
|
||||
rsc_layout,
|
||||
&self.array_view,
|
||||
&gpu.view,
|
||||
&self.sampler,
|
||||
masks,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_image(
|
||||
&self,
|
||||
image: &DynamicImage,
|
||||
layout: &BindGroupLayout,
|
||||
sampler: &Sampler,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> ImageGpu {
|
||||
let rgba = image.to_rgba8();
|
||||
let (width, height) = rgba.dimensions();
|
||||
@@ -130,112 +307,151 @@ impl GpuTextures {
|
||||
rgba.as_bytes(),
|
||||
);
|
||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
||||
let group = sampled_group(&self.device, layout, &view, sampler, "ui image");
|
||||
ImageGpu { texture, group }
|
||||
let bind_group = Self::make_image_bind_group(
|
||||
&self.device,
|
||||
rsc_layout,
|
||||
&self.array_view,
|
||||
&view,
|
||||
&self.sampler,
|
||||
masks,
|
||||
);
|
||||
ImageGpu { view, bind_group }
|
||||
}
|
||||
|
||||
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
||||
let Some(Some(slot)) = self.slots.get(i as usize) else {
|
||||
return;
|
||||
};
|
||||
let dst = TexelCopyTextureInfo {
|
||||
texture: &slot.texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
z: 0,
|
||||
/// Builds group 2 for one standalone image: the shared atlas array, this
|
||||
/// image's own view, the shared sampler, and the shared masks buffer --
|
||||
/// the same layout the main draw uses with a null view in the image slot.
|
||||
fn make_image_bind_group(
|
||||
device: &Device,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
array_view: &TextureView,
|
||||
image_view: &TextureView,
|
||||
sampler: &Sampler,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout: rsc_layout,
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::TextureView(array_view),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: BindingResource::TextureView(image_view),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: BindingResource::Sampler(sampler),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: masks.buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui rsc image"),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_array_texture(device: &Device, capacity: u32) -> Texture {
|
||||
device.create_texture(&TextureDescriptor {
|
||||
label: Some("glyph atlas array"),
|
||||
size: Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: capacity,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
};
|
||||
match image.as_rgba8() {
|
||||
Some(rgba) => write_region(&self.queue, dst, rgba, rect),
|
||||
// The texture is rgba8, so any other layout has to be converted --
|
||||
// and converting the rectangle is cheaper than the whole image.
|
||||
None => {
|
||||
let sub = image
|
||||
.view(rect.x, rect.y, rect.width, rect.height)
|
||||
.to_image();
|
||||
write_region(&self.queue, dst, &sub, PatchRect { x: 0, y: 0, ..rect });
|
||||
}
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING
|
||||
| TextureUsages::COPY_DST
|
||||
| TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
let sampler = default_sampler(device);
|
||||
let null_view = null_texture_view(device);
|
||||
let array_capacity = 1;
|
||||
let array_texture = Self::create_array_texture(device, array_capacity);
|
||||
let array_view = array_texture.create_view(&TextureViewDescriptor {
|
||||
dimension: Some(TextureViewDimension::D2Array),
|
||||
..Default::default()
|
||||
});
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
slots: Vec::new(),
|
||||
array_texture,
|
||||
array_view,
|
||||
array_capacity,
|
||||
page_count: 0,
|
||||
sampler,
|
||||
null_view,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn array_view(&self) -> &TextureView {
|
||||
&self.array_view
|
||||
}
|
||||
|
||||
pub fn null_view(&self) -> &TextureView {
|
||||
&self.null_view
|
||||
}
|
||||
|
||||
pub fn sampler(&self) -> &Sampler {
|
||||
&self.sampler
|
||||
}
|
||||
|
||||
/// The bind group a standalone image draws with. Panics if `idx` names an
|
||||
/// atlas page or a freed slot instead -- either is a caller bug (the
|
||||
/// wrong kind of instance reached this draw path), not a condition to
|
||||
/// recover from.
|
||||
pub fn image_bind_group(&self, idx: u32) -> &BindGroup {
|
||||
match self.slots.get(idx as usize) {
|
||||
Some(Slot::Image(gpu)) => &gpu.bind_group,
|
||||
other => panic!("texture slot {idx} is not a live standalone image: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_count(&self) -> usize {
|
||||
self.slots
|
||||
.iter()
|
||||
.filter(|s| !matches!(s, Slot::Empty))
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Slot {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Slot::Empty => write!(f, "Empty"),
|
||||
Slot::Image(_) => write!(f, "Image"),
|
||||
Slot::Page(layer) => write!(f, "Page(layer={layer})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) {
|
||||
if rect.width == 0 || rect.height == 0 {
|
||||
return;
|
||||
}
|
||||
let stride = src.width() * 4;
|
||||
queue.write_texture(
|
||||
dst,
|
||||
src.as_bytes(),
|
||||
TexelCopyBufferLayout {
|
||||
offset: (rect.y * stride + rect.x * 4) as u64,
|
||||
bytes_per_row: Some(stride),
|
||||
rows_per_image: Some(rect.height),
|
||||
},
|
||||
Extent3d {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// What a primitive that samples binds: a texture, and the sampler that reads
|
||||
/// it.
|
||||
pub fn sampled_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
view: &TextureView,
|
||||
sampler: &Sampler,
|
||||
label: &'static str,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::TextureView(view),
|
||||
pub fn null_texture_view(device: &Device) -> TextureView {
|
||||
device
|
||||
.create_texture(&TextureDescriptor {
|
||||
label: Some("null"),
|
||||
size: Extent3d {
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
label: Some(label),
|
||||
})
|
||||
}
|
||||
|
||||
/// The layout for one of those. The dimension differs -- the atlas is an
|
||||
/// array of pages and an image is not -- and nothing else does.
|
||||
pub fn sampled_layout(
|
||||
device: &Device,
|
||||
dimension: TextureViewDimension,
|
||||
label: &'static str,
|
||||
) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Texture {
|
||||
sample_type: TextureSampleType::Float { filterable: false },
|
||||
view_dimension: dimension,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some(label),
|
||||
})
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
})
|
||||
.create_view(&TextureViewDescriptor::default())
|
||||
}
|
||||
|
||||
pub fn default_sampler(device: &Device) -> Sampler {
|
||||
|
||||
@@ -21,8 +21,9 @@ impl<T: Pod> ArrBuf<T> {
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
/// Returns whether the `Buffer` was recreated, which stales any cached
|
||||
/// `BindGroup` holding it.
|
||||
/// Returns whether the underlying `Buffer` was recreated -- a caller that
|
||||
/// cached a `BindGroup` referencing it (as `GpuTextures` does for the
|
||||
/// masks buffer) needs to know to rebuild that too.
|
||||
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
|
||||
let resized = self.len != data.len();
|
||||
if resized {
|
||||
@@ -33,13 +34,9 @@ impl<T: Pod> ArrBuf<T> {
|
||||
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
|
||||
resized
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
|
||||
let mut size = size as u64;
|
||||
if usage.contains(BufferUsages::STORAGE) {
|
||||
// A binding cannot be empty or under the layout's minimum.
|
||||
size = size.max(std::mem::size_of::<T>() as u64);
|
||||
}
|
||||
device.create_buffer(&BufferDescriptor {
|
||||
@@ -49,4 +46,8 @@ impl<T: Pod> ArrBuf<T> {
|
||||
usage,
|
||||
})
|
||||
}
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
}
|
||||
+2
-6
@@ -1,6 +1,4 @@
|
||||
use crate::{
|
||||
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
||||
};
|
||||
use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena};
|
||||
|
||||
mod active;
|
||||
mod cache;
|
||||
@@ -9,15 +7,13 @@ mod render_state;
|
||||
mod size;
|
||||
|
||||
pub use active::*;
|
||||
pub use painter::{Painter, PrimitiveLike};
|
||||
pub use painter::Painter;
|
||||
pub use render_state::*;
|
||||
pub use size::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UiData {
|
||||
pub widgets: Widgets,
|
||||
/// Every primitive this ui can draw.
|
||||
pub primitives: PrimitiveRegistry,
|
||||
pub textures: Textures,
|
||||
pub text: TextData,
|
||||
pub masks: TrackedArena<Mask, u32>,
|
||||
|
||||
+41
-56
@@ -1,10 +1,7 @@
|
||||
use crate::{
|
||||
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
||||
render::{
|
||||
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
|
||||
TexturePrimitive,
|
||||
},
|
||||
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||
util::Vec2,
|
||||
};
|
||||
|
||||
@@ -24,26 +21,15 @@ pub struct Painter<'a> {
|
||||
|
||||
impl<'a> Painter<'a> {
|
||||
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||
let kind = self.rsc.ui_mut().primitives.kind::<P>();
|
||||
self.write(kind, primitive, region);
|
||||
}
|
||||
|
||||
/// Takes the kind, for a caller writing many of one primitive.
|
||||
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
|
||||
let h = self.state.layers.write(
|
||||
self.layer,
|
||||
PrimitiveInst {
|
||||
kind,
|
||||
id: self.id,
|
||||
primitive,
|
||||
region,
|
||||
mask_idx: self.mask,
|
||||
},
|
||||
);
|
||||
self.push_primitive(h);
|
||||
}
|
||||
|
||||
fn push_primitive(&mut self, h: PrimitiveHandle) {
|
||||
if self.mask != MaskIdx::NONE {
|
||||
// TODO: I have no clue if this works at all :joy:
|
||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||
@@ -52,13 +38,11 @@ impl<'a> Painter<'a> {
|
||||
}
|
||||
|
||||
/// Writes a primitive to be rendered
|
||||
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
|
||||
let primitive = primitive.into_primitive(self);
|
||||
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
|
||||
self.primitive_at(primitive, self.region)
|
||||
}
|
||||
|
||||
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
|
||||
let primitive = primitive.into_primitive(self);
|
||||
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||
self.primitive_at(primitive, region.within(&self.region));
|
||||
}
|
||||
|
||||
@@ -91,6 +75,35 @@ impl<'a> Painter<'a> {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
self.textures.push(handle.clone());
|
||||
self.write_image(handle.image_index(), region.within(&self.region));
|
||||
}
|
||||
|
||||
pub fn texture(&mut self, handle: &TextureHandle) {
|
||||
self.textures.push(handle.clone());
|
||||
self.write_image(handle.image_index(), self.region);
|
||||
}
|
||||
|
||||
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
self.textures.push(handle.clone());
|
||||
self.write_image(handle.image_index(), region);
|
||||
}
|
||||
|
||||
/// A standalone image draws with its own bind group rather than sharing
|
||||
/// the layer's one instanced draw, so it goes through
|
||||
/// `Primitives::write_image` instead of `primitive_at`/`Primitive::vec`.
|
||||
fn write_image(&mut self, texture_idx: u32, region: UiRegion) {
|
||||
let h = self
|
||||
.state
|
||||
.layers
|
||||
.write_image(self.layer, self.id, texture_idx, region, self.mask);
|
||||
if self.mask != MaskIdx::NONE {
|
||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||
}
|
||||
self.primitives.push(h);
|
||||
}
|
||||
|
||||
pub fn render_text(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
@@ -98,12 +111,10 @@ impl<'a> Painter<'a> {
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
let ui = self.rsc.ui_mut();
|
||||
ui.text.render(buffer, attrs, width)
|
||||
ui.text.render(buffer, attrs, width, &mut ui.textures)
|
||||
}
|
||||
|
||||
// TODO: merge the text methods into the primitive ones.
|
||||
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
||||
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
|
||||
for glyph in text.glyphs.iter() {
|
||||
let mut region = origin;
|
||||
region.x.end = region.x.start;
|
||||
@@ -111,15 +122,14 @@ impl<'a> Painter<'a> {
|
||||
let mut region = region.offset(UiVec2::abs(glyph.offset));
|
||||
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
|
||||
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
|
||||
self.write(
|
||||
kind,
|
||||
GlyphPrimitive {
|
||||
uv_min: glyph.entry.uv_min,
|
||||
uv_max: glyph.entry.uv_max,
|
||||
layer: glyph.entry.layer,
|
||||
color: text.color,
|
||||
flags: glyph.entry.flags(),
|
||||
},
|
||||
self.primitive_at(
|
||||
GlyphPrimitive::new(
|
||||
glyph.entry.uv_min,
|
||||
glyph.entry.uv_max,
|
||||
glyph.entry.layer,
|
||||
text.color,
|
||||
glyph.entry.flags(),
|
||||
),
|
||||
region,
|
||||
);
|
||||
}
|
||||
@@ -172,28 +182,3 @@ impl<'a> Painter<'a> {
|
||||
self.state.size_ctx(self.id, self.region.size(), self.rsc)
|
||||
}
|
||||
}
|
||||
|
||||
/// What `Painter::primitive` takes: a primitive, or something that yields one
|
||||
/// and does whatever else drawing it needs.
|
||||
pub trait PrimitiveLike {
|
||||
type Primitive: Primitive;
|
||||
fn into_primitive(self, painter: &mut Painter) -> Self::Primitive;
|
||||
}
|
||||
|
||||
impl<P: Primitive> PrimitiveLike for P {
|
||||
type Primitive = P;
|
||||
fn into_primitive(self, _: &mut Painter) -> P {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveLike for &TextureHandle {
|
||||
type Primitive = TexturePrimitive;
|
||||
|
||||
/// Retains a share of the handle, so the slot the primitive names cannot
|
||||
/// be freed and reused while it is still drawn.
|
||||
fn into_primitive(self, painter: &mut Painter) -> TexturePrimitive {
|
||||
painter.textures.push(self.clone());
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
+11
-10
@@ -1,13 +1,13 @@
|
||||
use crate::{
|
||||
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget,
|
||||
UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
||||
ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx,
|
||||
StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
||||
ui::cache::Cache,
|
||||
util::{HashMap, HashSet, Vec2, forget_ref},
|
||||
};
|
||||
|
||||
pub struct UiRenderState {
|
||||
pub active: HashMap<WidgetId, ActiveData>,
|
||||
pub layers: DrawLayers,
|
||||
pub layers: PrimitiveLayers,
|
||||
pub(super) output_size: Vec2,
|
||||
pub cache: Cache,
|
||||
|
||||
@@ -243,14 +243,14 @@ impl UiRenderState {
|
||||
}
|
||||
|
||||
pub fn debug_layers(&self) {
|
||||
for ((idx, depth), draws) in self.layers.iter_depth() {
|
||||
for ((idx, depth), primitives) in self.layers.iter_depth() {
|
||||
let indent = " ".repeat(depth * 2);
|
||||
let counts: Vec<String> = draws
|
||||
.primitives()
|
||||
.iter()
|
||||
.map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string())
|
||||
.collect();
|
||||
println!("{indent}{idx}: [{}]", counts.join(", "));
|
||||
let len = primitives.instances().len();
|
||||
print!("{indent}{idx}: {len} primitives");
|
||||
if len >= 1 {
|
||||
print!(" ({})", primitives.instances()[0].binding);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +308,7 @@ impl UiRenderState {
|
||||
source,
|
||||
cache: &mut self.cache,
|
||||
text: &mut ui.text,
|
||||
textures: &mut ui.textures,
|
||||
widgets: &ui.widgets,
|
||||
outer,
|
||||
output_size: self.output_size,
|
||||
|
||||
+5
-3
@@ -1,10 +1,11 @@
|
||||
use crate::{
|
||||
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
|
||||
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
||||
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
|
||||
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
||||
};
|
||||
|
||||
pub struct SizeCtx<'a> {
|
||||
pub text: &'a mut TextData,
|
||||
pub textures: &'a mut Textures,
|
||||
pub(super) source: WidgetId,
|
||||
pub(super) widgets: &'a Widgets,
|
||||
pub(super) cache: &'a mut Cache,
|
||||
@@ -32,6 +33,7 @@ impl SizeCtx<'_> {
|
||||
.get_dyn_dynamic(id)
|
||||
.desired_len::<A>(&mut SizeCtx {
|
||||
text: self.text,
|
||||
textures: self.textures,
|
||||
source: self.source,
|
||||
widgets: self.widgets,
|
||||
cache: self.cache,
|
||||
@@ -80,7 +82,7 @@ impl SizeCtx<'_> {
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
self.text.render(buffer, attrs, width)
|
||||
self.text.render(buffer, attrs, width, self.textures)
|
||||
}
|
||||
|
||||
pub fn label(&self, id: WidgetId) -> &String {
|
||||
|
||||
@@ -212,10 +212,10 @@ impl DefaultAppState for Client {
|
||||
render: &mut UiRenderState,
|
||||
) {
|
||||
let new = format!(
|
||||
"widgets: {}\nactive: {}\ntextures: {}",
|
||||
"widgets: {}\nactive: {}\nviews: {}",
|
||||
rsc.widgets().len(),
|
||||
render.active_widgets(),
|
||||
rsc.ui().textures.count(),
|
||||
self.ui_state.renderer.ui.view_count(),
|
||||
);
|
||||
if new != *rsc.widgets()[self.info].content {
|
||||
*rsc.widgets_mut()[self.info].content = new;
|
||||
|
||||
+11
-21
@@ -22,20 +22,7 @@ impl UiRenderer {
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
let output = match self.surface.get_current_texture() {
|
||||
CurrentSurfaceTexture::Success(texture) => texture,
|
||||
CurrentSurfaceTexture::Suboptimal(texture) => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
texture
|
||||
}
|
||||
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
return;
|
||||
}
|
||||
CurrentSurfaceTexture::Timeout
|
||||
| CurrentSurfaceTexture::Occluded
|
||||
| CurrentSurfaceTexture::Validation => return,
|
||||
};
|
||||
let output = self.surface.get_current_texture().unwrap();
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&TextureViewDescriptor::default());
|
||||
@@ -59,7 +46,7 @@ impl UiRenderer {
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.window.pre_present_notify();
|
||||
self.queue.present(output);
|
||||
output.present();
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
||||
@@ -78,10 +65,9 @@ impl UiRenderer {
|
||||
pub fn new(window: Arc<Window>) -> Self {
|
||||
let size = window.inner_size();
|
||||
|
||||
let instance = Instance::new(InstanceDescriptor {
|
||||
let instance = Instance::new(&InstanceDescriptor {
|
||||
backends: Backends::PRIMARY,
|
||||
display: Some(Box::new(window.clone())),
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let surface = instance
|
||||
@@ -93,11 +79,16 @@ impl UiRenderer {
|
||||
power_preference: PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
apply_limit_buckets: false,
|
||||
})
|
||||
.block_on()
|
||||
.expect("Could not get adapter!");
|
||||
|
||||
// No features beyond what wgpu asks for by default, and no
|
||||
// binding-array limits: the atlas is one texture_2d_array and a
|
||||
// standalone image is its own ordinary bind group, neither of which
|
||||
// needs descriptor indexing. The binding array this replaced asked
|
||||
// for VK_EXT_descriptor_indexing unconditionally and so did not run
|
||||
// on a real share of Android GPUs.
|
||||
let (device, queue) = adapter
|
||||
.request_device(&DeviceDescriptor {
|
||||
required_limits: Limits {
|
||||
@@ -120,7 +111,6 @@ impl UiRenderer {
|
||||
let config = SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: PresentMode::AutoVsync,
|
||||
@@ -133,7 +123,7 @@ impl UiRenderer {
|
||||
|
||||
let encoder = Self::create_encoder(&device);
|
||||
|
||||
let ui = UiRenderNode::new(&device, &config);
|
||||
let ui = UiRenderNode::new(&device, &queue, &config);
|
||||
|
||||
Self {
|
||||
surface,
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ pub struct Image {
|
||||
|
||||
impl Widget for Image {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
painter.primitive(&self.handle);
|
||||
painter.texture(&self.handle);
|
||||
}
|
||||
|
||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
//! What one frame of `UiRenderNode::draw` costs on the CPU, against the number
|
||||
//! of layers it walks. Recording only: the pass is built and dropped without
|
||||
//! being submitted, so this is the loop's cost and not the GPU's.
|
||||
//!
|
||||
//! cargo test --release --test draw_cost -- --ignored --nocapture
|
||||
//!
|
||||
//! Wall time is the wrong number to read for anything under a few percent --
|
||||
//! it varied by 2x between runs of one unchanged binary where instructions
|
||||
//! retired varied by 0.1%. Count those instead:
|
||||
//!
|
||||
//! perf stat -e instructions:u target/release/.../draw_cost-* --ignored
|
||||
//!
|
||||
//! That is how `PrimitiveRender` was measured against a match in the renderer:
|
||||
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
|
||||
//! one.
|
||||
//!
|
||||
//! The instance is leaked deliberately. A Vulkan loader may unload the driver
|
||||
//! when the last one drops, which can fault as a thread that used it exits --
|
||||
//! and every test runs on a spawned thread.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use iris::prelude::*;
|
||||
use iris_core::{
|
||||
GlyphPrimitive, MaskIdx, PrimitiveInst, RectPrimitive, TextureHandle, TexturePrimitive, UiData,
|
||||
UiRegion, UiRenderNode, UiRenderState,
|
||||
};
|
||||
use wgpu::{Color as GpuColor, *};
|
||||
|
||||
const SIZE: u32 = 1024;
|
||||
const FRAMES: u32 = 200;
|
||||
/// Reported as the best of this many batches, since the mean moves by more
|
||||
/// than the thing being measured.
|
||||
const BATCHES: u32 = 8;
|
||||
|
||||
fn gpu() -> Option<(Device, Queue)> {
|
||||
// Probed rather than assumed: there may be no Vulkan adapter, and GL is
|
||||
// what is left when there is not.
|
||||
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
||||
{
|
||||
Ok(_) => all,
|
||||
Err(_) => Instance::new(InstanceDescriptor {
|
||||
backends: Backends::GL,
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
}),
|
||||
};
|
||||
// Leaked rather than dropped: see the note at the top of the file.
|
||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
||||
let adapter =
|
||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
||||
println!("adapter: {:?}", adapter.get_info());
|
||||
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
|
||||
}
|
||||
|
||||
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
||||
SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format,
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
width: SIZE,
|
||||
height: SIZE,
|
||||
present_mode: PresentMode::Fifo,
|
||||
desired_maximum_frame_latency: 2,
|
||||
alpha_mode: CompositeAlphaMode::Auto,
|
||||
view_formats: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Every layer draws all three primitives, so the renderer takes a different
|
||||
/// path for each list it walks -- which is the case a single-primitive layer
|
||||
/// would never exercise. Images are bound per instance, so there are few.
|
||||
fn fill(
|
||||
ui: &mut UiData,
|
||||
render: &mut UiRenderState,
|
||||
layers: usize,
|
||||
per_layer: usize,
|
||||
) -> Vec<TextureHandle> {
|
||||
let rect = ui.primitives.kind::<RectPrimitive>();
|
||||
let glyph = ui.primitives.kind::<GlyphPrimitive>();
|
||||
let texture = ui.primitives.kind::<TexturePrimitive>();
|
||||
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
||||
let handles: Vec<_> = (0..4)
|
||||
.map(|_| ui.textures.add(image::RgbaImage::new(4, 4)))
|
||||
.collect();
|
||||
|
||||
let mut layer = 0;
|
||||
for _ in 0..layers {
|
||||
for _ in 0..per_layer {
|
||||
render.layers.write(
|
||||
layer,
|
||||
PrimitiveInst {
|
||||
kind: rect,
|
||||
id,
|
||||
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||
region: UiRegion::FULL,
|
||||
mask_idx: MaskIdx::NONE,
|
||||
},
|
||||
);
|
||||
render.layers.write(
|
||||
layer,
|
||||
PrimitiveInst {
|
||||
kind: glyph,
|
||||
id,
|
||||
primitive: GlyphPrimitive {
|
||||
uv_min: vec2(0.0, 0.0),
|
||||
uv_max: vec2(1.0, 1.0),
|
||||
layer: 0,
|
||||
color: UiColor::WHITE,
|
||||
flags: 0,
|
||||
},
|
||||
region: UiRegion::FULL,
|
||||
mask_idx: MaskIdx::NONE,
|
||||
},
|
||||
);
|
||||
}
|
||||
for h in &handles[..2] {
|
||||
render.layers.write(
|
||||
layer,
|
||||
PrimitiveInst {
|
||||
kind: texture,
|
||||
id,
|
||||
primitive: TexturePrimitive::from(h),
|
||||
region: UiRegion::FULL,
|
||||
mask_idx: MaskIdx::NONE,
|
||||
},
|
||||
);
|
||||
}
|
||||
layer = render.layers.next(layer);
|
||||
}
|
||||
handles
|
||||
}
|
||||
|
||||
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
|
||||
let format = TextureFormat::Bgra8Unorm;
|
||||
let mut node = UiRenderNode::new(device, &config(format));
|
||||
let mut ui = UiData::default();
|
||||
let mut render = UiRenderState::new();
|
||||
let _handles = fill(&mut ui, &mut render, layers, per_layer);
|
||||
node.update(device, queue, &mut ui, &mut render);
|
||||
|
||||
let target = device.create_texture(&TextureDescriptor {
|
||||
label: Some("draw cost"),
|
||||
size: Extent3d {
|
||||
width: SIZE,
|
||||
height: SIZE,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format,
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = target.create_view(&TextureViewDescriptor::default());
|
||||
|
||||
let record = |frames: u32| {
|
||||
let start = Instant::now();
|
||||
for _ in 0..frames {
|
||||
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
||||
{
|
||||
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(GpuColor::BLACK),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
node.draw(pass);
|
||||
}
|
||||
drop(encoder.finish());
|
||||
}
|
||||
start.elapsed().as_secs_f64() / frames as f64
|
||||
};
|
||||
record(FRAMES / 4);
|
||||
(0..BATCHES)
|
||||
.map(|_| record(FRAMES))
|
||||
.fold(f64::MAX, f64::min)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn draw_cost_by_layer_count() {
|
||||
let Some((device, queue)) = gpu() else {
|
||||
panic!("no wgpu device; see the this-machine-graphics notes");
|
||||
};
|
||||
println!(
|
||||
"layers, each 8 rects + 8 glyphs + 2 images: us/frame (us per layer), best of {BATCHES}"
|
||||
);
|
||||
let base = frame_cost(&device, &queue, 1, 8) * 1e6;
|
||||
for layers in [8, 64, 256, 1024] {
|
||||
let per_frame = frame_cost(&device, &queue, layers, 8) * 1e6;
|
||||
// Net of the empty pass, which is the same in any version of this.
|
||||
println!(
|
||||
"{layers:>5}: {per_frame:8.1} us ({:.3} us)",
|
||||
(per_frame - base).max(0.0) / layers as f64
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user