Compare commits

..
Author SHA1 Message Date
iris bafaa1db6d Draw the glyph atlas as an array texture and images with their own bind groups
The renderer bound every texture through one
`binding_array<texture_2d<f32>>` indexed per primitive. That needs
`VK_EXT_descriptor_indexing`, which a real share of Android GPUs do not
have, so the shape did not run there at all.

Split the two things being bound, since they want opposite treatment:

- Glyph atlas pages become layers of one `texture_2d_array`. A glyph
  primitive carries a layer rather than a view/sampler index pair, and a
  layer index is an ordinary sampling operand -- no extension. Growing the
  atlas recreates the array with headroom and copies the old layers across
  GPU-side.
- A standalone image gets its own texture and its own bind group, and
  draws in its own call. It no longer needs a per-instance entry in
  `PrimitiveData` at all: the bind group has already picked the texture.

`Primitives` therefore keeps images in a list of their own, with
`PrimitiveChange::is_image` saying which list a renumbering belongs to --
the two have independent index spaces, so `(layer, inst_idx)` alone would
collide between them.

Verified on this machine's real GPU (Venus onto an RX 7900 XT, confirmed
by the loaded ICD rather than assumed): the `tabs` example renders
byte-identical screenshots before and after, both for a text-and-rect tab
and for one holding a standalone image.
2026-09-13 03:58:12 -04:00
31 changed files with 1664 additions and 1630 deletions

No files matched your search

Generated
+229 -203
View File
@@ -234,18 +234,18 @@ dependencies = [
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.10.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [ dependencies = [
"bit-vec", "bit-vec",
] ]
[[package]] [[package]]
name = "bit-vec" name = "bit-vec"
version = "0.9.1" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]] [[package]]
name = "bit_field" name = "bit_field"
@@ -274,6 +274,12 @@ dependencies = [
"core2", "core2",
] ]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]] [[package]]
name = "block2" name = "block2"
version = "0.5.1" version = "0.5.1"
@@ -283,15 +289,6 @@ dependencies = [
"objc2 0.5.2", "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]] [[package]]
name = "built" name = "built"
version = "0.8.0" version = "0.8.0"
@@ -394,9 +391,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]] [[package]]
name = "cfg_aliases" name = "cfg_aliases"
version = "0.2.2" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "clipboard-win" name = "clipboard-win"
@@ -409,9 +406,9 @@ dependencies = [
[[package]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.13.1" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
dependencies = [ dependencies = [
"serde", "serde",
"termcolor", "termcolor",
@@ -453,6 +450,16 @@ dependencies = [
"libc", "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]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@@ -466,8 +473,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation 0.9.4",
"core-graphics-types", "core-graphics-types 0.1.3",
"foreign-types", "foreign-types",
"libc", "libc",
] ]
@@ -479,7 +486,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [ dependencies = [
"bitflags 1.3.2", "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", "libc",
] ]
@@ -721,6 +739,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" 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]] [[package]]
name = "font-types" name = "font-types"
version = "0.12.4" version = "0.12.4"
@@ -744,7 +771,7 @@ dependencies = [
"objc2-core-text", "objc2-core-text",
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
"parlance", "parlance",
"read-fonts", "read-fonts 0.41.0",
"roxmltree", "roxmltree",
"smallvec", "smallvec",
"windows", "windows",
@@ -779,30 +806,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" 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]] [[package]]
name = "fxhash" name = "fxhash"
version = "0.2.1" version = "0.2.1"
@@ -857,9 +860,9 @@ dependencies = [
[[package]] [[package]]
name = "glow" name = "glow"
version = "0.17.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"slotmap", "slotmap",
@@ -890,6 +893,26 @@ dependencies = [
"windows", "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]] [[package]]
name = "half" name = "half"
version = "2.7.1" version = "2.7.1"
@@ -910,7 +933,7 @@ checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"bytemuck", "bytemuck",
"read-fonts", "read-fonts 0.41.0",
"smallvec", "smallvec",
] ]
@@ -949,6 +972,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hexf-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.3.0" version = "2.3.0"
@@ -1079,9 +1108,9 @@ checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad"
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.10" version = "0.25.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
@@ -1097,8 +1126,8 @@ dependencies = [
"rayon", "rayon",
"rgb", "rgb",
"tiff", "tiff",
"zune-core", "zune-core 0.5.0",
"zune-jpeg", "zune-jpeg 0.5.8",
] ]
[[package]] [[package]]
@@ -1218,12 +1247,11 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.105" version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [ dependencies = [
"cfg-if", "once_cell",
"futures-util",
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -1252,9 +1280,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.189" version = "0.2.179"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
[[package]] [[package]]
name = "libfuzzer-sys" name = "libfuzzer-sys"
@@ -1347,6 +1375,15 @@ dependencies = [
"imgref", "imgref",
] ]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "maybe-rayon" name = "maybe-rayon"
version = "0.1.1" version = "0.1.1"
@@ -1372,6 +1409,21 @@ dependencies = [
"libc", "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]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -1384,9 +1436,9 @@ dependencies = [
[[package]] [[package]]
name = "moxcms" name = "moxcms"
version = "0.8.1" version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [ dependencies = [
"num-traits", "num-traits",
"pxfm", "pxfm",
@@ -1394,9 +1446,9 @@ dependencies = [
[[package]] [[package]]
name = "naga" name = "naga"
version = "30.0.1" version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bit-set", "bit-set",
@@ -1405,11 +1457,11 @@ dependencies = [
"cfg_aliases", "cfg_aliases",
"codespan-reporting", "codespan-reporting",
"half", "half",
"hashbrown 0.17.1", "hashbrown 0.16.1",
"hexf-parse",
"indexmap", "indexmap",
"libm", "libm",
"log", "log",
"naga-types",
"num-traits", "num-traits",
"once_cell", "once_cell",
"rustc-hash", "rustc-hash",
@@ -1418,18 +1470,6 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "ndk" name = "ndk"
version = "0.9.0" version = "0.9.0"
@@ -1554,6 +1594,15 @@ dependencies = [
"syn 2.0.113", "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]] [[package]]
name = "objc-sys" name = "objc-sys"
version = "0.3.5" version = "0.3.5"
@@ -1586,13 +1635,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2 0.5.1", "block2",
"libc", "libc",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-data", "objc2-core-data",
"objc2-core-image", "objc2-core-image",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-quartz-core 0.2.2", "objc2-quartz-core",
] ]
[[package]] [[package]]
@@ -1614,7 +1663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2 0.5.1", "block2",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-location", "objc2-core-location",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -1626,7 +1675,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [ dependencies = [
"block2 0.5.1", "block2",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -1638,7 +1687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2 0.5.1", "block2",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -1673,10 +1722,10 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [ dependencies = [
"block2 0.5.1", "block2",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-metal 0.2.2", "objc2-metal",
] ]
[[package]] [[package]]
@@ -1685,7 +1734,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [ dependencies = [
"block2 0.5.1", "block2",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-contacts", "objc2-contacts",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -1714,7 +1763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2 0.5.1", "block2",
"dispatch", "dispatch",
"libc", "libc",
"objc2 0.5.2", "objc2 0.5.2",
@@ -1748,7 +1797,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [ dependencies = [
"block2 0.5.1", "block2",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-app-kit 0.2.2", "objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -1761,23 +1810,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2 0.5.1", "block2",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.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]] [[package]]
name = "objc2-quartz-core" name = "objc2-quartz-core"
version = "0.2.2" version = "0.2.2"
@@ -1785,24 +1822,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2 0.5.1", "block2",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-metal 0.2.2", "objc2-metal",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
dependencies = [
"bitflags 2.10.0",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
"objc2-metal 0.3.2",
] ]
[[package]] [[package]]
@@ -1822,7 +1845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2 0.5.1", "block2",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-cloud-kit", "objc2-cloud-kit",
"objc2-core-data", "objc2-core-data",
@@ -1830,7 +1853,7 @@ dependencies = [
"objc2-core-location", "objc2-core-location",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-link-presentation", "objc2-link-presentation",
"objc2-quartz-core 0.2.2", "objc2-quartz-core",
"objc2-symbols", "objc2-symbols",
"objc2-uniform-type-identifiers", "objc2-uniform-type-identifiers",
"objc2-user-notifications", "objc2-user-notifications",
@@ -1842,7 +1865,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [ dependencies = [
"block2 0.5.1", "block2",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -1854,7 +1877,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2 0.5.1", "block2",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-location", "objc2-core-location",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -1948,7 +1971,7 @@ dependencies = [
"linebender_resource_handle", "linebender_resource_handle",
"parlance", "parlance",
"parley_data", "parley_data",
"skrifa", "skrifa 0.44.0",
] ]
[[package]] [[package]]
@@ -2252,9 +2275,9 @@ dependencies = [
[[package]] [[package]]
name = "ravif" name = "ravif"
version = "0.13.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285"
dependencies = [ dependencies = [
"avif-serialize", "avif-serialize",
"imgref", "imgref",
@@ -2271,18 +2294,6 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" 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]] [[package]]
name = "rayon" name = "rayon"
version = "1.11.0" version = "1.11.0"
@@ -2303,6 +2314,16 @@ dependencies = [
"crossbeam-utils", "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]] [[package]]
name = "read-fonts" name = "read-fonts"
version = "0.41.0" version = "0.41.0"
@@ -2310,7 +2331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"font-types", "font-types 0.12.4",
"once_cell", "once_cell",
] ]
@@ -2485,6 +2506,16 @@ dependencies = [
"quote", "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]] [[package]]
name = "skrifa" name = "skrifa"
version = "0.44.0" version = "0.44.0"
@@ -2492,7 +2523,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"read-fonts", "read-fonts 0.41.0",
] ]
[[package]] [[package]]
@@ -2552,9 +2583,9 @@ dependencies = [
[[package]] [[package]]
name = "spirv" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
] ]
@@ -2583,7 +2614,7 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e"
dependencies = [ dependencies = [
"skrifa", "skrifa 0.39.0",
"yazi", "yazi",
"zeno", "zeno",
] ]
@@ -2672,16 +2703,16 @@ dependencies = [
[[package]] [[package]]
name = "tiff" name = "tiff"
version = "0.11.3" version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
dependencies = [ dependencies = [
"fax", "fax",
"flate2", "flate2",
"half", "half",
"quick-error", "quick-error",
"weezl", "weezl",
"zune-jpeg", "zune-jpeg 0.4.21",
] ]
[[package]] [[package]]
@@ -2854,9 +2885,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.128" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -2867,19 +2898,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.78" version = "0.4.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
dependencies = [ dependencies = [
"cfg-if",
"js-sys", "js-sys",
"once_cell",
"wasm-bindgen", "wasm-bindgen",
"web-sys",
] ]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.128" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -2887,22 +2921,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.128" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.5", "syn 2.0.113",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.128" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -3018,9 +3052,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.105" version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -3044,9 +3078,9 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]] [[package]]
name = "wgpu" name = "wgpu"
version = "30.0.1" version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bitflags 2.10.0", "bitflags 2.10.0",
@@ -3054,7 +3088,7 @@ dependencies = [
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"document-features", "document-features",
"hashbrown 0.17.1", "hashbrown 0.16.1",
"js-sys", "js-sys",
"log", "log",
"naga", "naga",
@@ -3074,9 +3108,9 @@ dependencies = [
[[package]] [[package]]
name = "wgpu-core" name = "wgpu-core"
version = "30.0.1" version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" checksum = "8bb4c8b5db5f00e56f1f08869d870a0dff7c8bc7ebc01091fec140b0cf0211a9"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bit-set", "bit-set",
@@ -3085,11 +3119,10 @@ dependencies = [
"bytemuck", "bytemuck",
"cfg_aliases", "cfg_aliases",
"document-features", "document-features",
"hashbrown 0.17.1", "hashbrown 0.16.1",
"indexmap", "indexmap",
"log", "log",
"naga", "naga",
"naga-types",
"once_cell", "once_cell",
"parking_lot", "parking_lot",
"portable-atomic", "portable-atomic",
@@ -3102,70 +3135,66 @@ dependencies = [
"wgpu-core-deps-emscripten", "wgpu-core-deps-emscripten",
"wgpu-core-deps-windows-linux-android", "wgpu-core-deps-windows-linux-android",
"wgpu-hal", "wgpu-hal",
"wgpu-naga-bridge",
"wgpu-types", "wgpu-types",
] ]
[[package]] [[package]]
name = "wgpu-core-deps-apple" name = "wgpu-core-deps-apple"
version = "30.0.1" version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da"
dependencies = [ dependencies = [
"wgpu-hal", "wgpu-hal",
] ]
[[package]] [[package]]
name = "wgpu-core-deps-emscripten" name = "wgpu-core-deps-emscripten"
version = "30.0.1" version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86" checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79"
dependencies = [ dependencies = [
"wgpu-hal", "wgpu-hal",
] ]
[[package]] [[package]]
name = "wgpu-core-deps-windows-linux-android" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae"
dependencies = [ dependencies = [
"wgpu-hal", "wgpu-hal",
] ]
[[package]] [[package]]
name = "wgpu-hal" name = "wgpu-hal"
version = "30.0.1" version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" checksum = "293080d77fdd14d6b08a67c5487dfddbf874534bb7921526db56a7b75d7e3bef"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"arrayvec", "arrayvec",
"ash", "ash",
"bit-set", "bit-set",
"bitflags 2.10.0", "bitflags 2.10.0",
"block2 0.6.2", "block",
"bytemuck", "bytemuck",
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"core-graphics-types 0.2.0",
"glow", "glow",
"glutin_wgl_sys", "glutin_wgl_sys",
"gpu-allocator", "gpu-allocator",
"hashbrown 0.17.1", "gpu-descriptor",
"hashbrown 0.16.1",
"js-sys", "js-sys",
"khronos-egl", "khronos-egl",
"libc", "libc",
"libloading", "libloading",
"log", "log",
"metal",
"naga", "naga",
"naga-types",
"ndk-sys", "ndk-sys",
"objc2 0.6.4", "objc",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
"objc2-metal 0.3.2",
"objc2-quartz-core 0.3.2",
"once_cell", "once_cell",
"ordered-float", "ordered-float",
"parking_lot", "parking_lot",
@@ -3174,44 +3203,26 @@ dependencies = [
"profiling", "profiling",
"range-alloc", "range-alloc",
"raw-window-handle", "raw-window-handle",
"raw-window-metal",
"renderdoc-sys", "renderdoc-sys",
"smallvec", "smallvec",
"static_assertions",
"thiserror 2.0.17", "thiserror 2.0.17",
"wasm-bindgen", "wasm-bindgen",
"wayland-sys",
"web-sys", "web-sys",
"wgpu-naga-bridge",
"wgpu-types", "wgpu-types",
"windows", "windows",
"windows-core", "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]] [[package]]
name = "wgpu-types" name = "wgpu-types"
version = "30.0.1" version = "28.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"bytemuck", "bytemuck",
"js-sys", "js-sys",
"log", "log",
"naga-types",
"raw-window-handle",
"static_assertions",
"web-sys", "web-sys",
] ]
@@ -3575,12 +3586,12 @@ dependencies = [
"android-activity", "android-activity",
"atomic-waker", "atomic-waker",
"bitflags 2.10.0", "bitflags 2.10.0",
"block2 0.5.1", "block2",
"bytemuck", "bytemuck",
"calloop", "calloop",
"cfg_aliases", "cfg_aliases",
"concurrent-queue", "concurrent-queue",
"core-foundation", "core-foundation 0.9.4",
"core-graphics", "core-graphics",
"cursor-icon", "cursor-icon",
"dpi", "dpi",
@@ -3847,6 +3858,12 @@ dependencies = [
"syn 3.0.5", "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]] [[package]]
name = "zune-core" name = "zune-core"
version = "0.5.0" version = "0.5.0"
@@ -3862,11 +3879,20 @@ dependencies = [
"simd-adler32", "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]] [[package]]
name = "zune-jpeg" name = "zune-jpeg"
version = "0.5.8" version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5"
dependencies = [ dependencies = [
"zune-core", "zune-core 0.5.0",
] ]
+2 -2
View File
@@ -29,9 +29,9 @@ edition = "2024"
[workspace.dependencies] [workspace.dependencies]
pollster = "0.4.0" pollster = "0.4.0"
winit = "0.30.12" winit = "0.30.12"
wgpu = "30.0.1" wgpu = "28.0.0"
bytemuck = "1.23.1" bytemuck = "1.23.1"
image = "0.25.10" image = "0.25.6"
parley = "0.11.1" parley = "0.11.1"
swash = "0.2.10" swash = "0.2.10"
fxhash = "0.2.1" fxhash = "0.2.1"
+15 -3
View File
@@ -1,7 +1,8 @@
use std::ops::{Index, IndexMut}; use std::ops::{Index, IndexMut};
use crate::{ use crate::{
render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, UiRegion, WidgetId,
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
util::to_mut, util::to_mut,
}; };
@@ -39,7 +40,7 @@ struct Child {
tail: usize, tail: usize,
} }
pub type DrawLayers = Layers<LayerDraws>; pub type PrimitiveLayers = Layers<Primitives>;
impl<T: Default> Layers<T> { impl<T: Default> Layers<T> {
pub fn new() -> 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>( pub fn write<P: Primitive>(
&mut self, &mut self,
layer: LayerId, layer: LayerId,
@@ -131,6 +132,17 @@ impl DrawLayers {
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self[h.layer].free(h) 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> { impl<T: Default> Default for Layers<T> {
+16 -7
View File
@@ -1,5 +1,6 @@
use crate::{ use crate::{
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2, Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor,
util::Vec2,
}; };
use parley::{ use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
@@ -158,7 +159,7 @@ impl TextBuffer {
} }
impl TextData { 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(); let mut placed = Vec::new();
for line in buffer.layout.lines() { for line in buffer.layout.lines() {
for item in line.items() { for item in line.items() {
@@ -184,14 +185,17 @@ impl TextData {
subpixel, subpixel,
coords: coords_hash, coords: coords_hash,
}; };
let Some(entry) = self.glyph_entry(GlyphRaster { let Some(entry) = self.glyph_entry(
GlyphRaster {
key, key,
font: font_ref, font: font_ref,
font_size, font_size,
coords, coords,
subpixel, subpixel,
glyph_id: glyph.id, glyph_id: glyph.id,
}) else { },
textures,
) else {
continue; continue;
}; };
placed.push(PlacedGlyph { placed.push(PlacedGlyph {
@@ -207,7 +211,11 @@ impl TextData {
placed 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) { if let Some(entry) = self.atlas.get(&glyph.key) {
return entry; return entry;
} }
@@ -229,7 +237,7 @@ impl TextData {
.render(&mut scaler, glyph.glyph_id as u16); .render(&mut scaler, glyph.glyph_id as u16);
if let Some(image) = image { if let Some(image) = image {
self.atlas.insert(glyph.key, &image) self.atlas.insert(glyph.key, &image, textures)
} else { } else {
self.atlas.insert_empty(glyph.key); self.atlas.insert_empty(glyph.key);
None None
@@ -270,9 +278,10 @@ impl TextData {
buffer: &mut TextBuffer, buffer: &mut TextBuffer,
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, width: Option<f32>,
textures: &mut Textures,
) -> RenderedText { ) -> RenderedText {
buffer.shape(self, attrs, width); buffer.shape(self, attrs, width);
let glyphs = self.place(buffer); let glyphs = self.place(buffer, textures);
RenderedText { RenderedText {
glyphs, glyphs,
size: buffer.size(), size: buffer.size(),
+93 -31
View File
@@ -5,12 +5,28 @@ use std::{
sync::mpsc::{Receiver, Sender, channel}, 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)] #[derive(Debug, Clone)]
pub struct TextureHandle { pub struct TextureHandle {
slot: u32, slot: u32,
kind: TextureKind,
size: Vec2, size: Vec2,
counter: RefCounter, counter: RefCounter,
send: Sender<u32>, send: Sender<(TextureKind, u32)>,
} }
/// a texture manager for a ui /// a texture manager for a ui
@@ -18,19 +34,24 @@ pub struct TextureHandle {
pub struct Textures { pub struct Textures {
free: Vec<u32>, free: Vec<u32>,
images: Vec<Option<DynamicImage>>, 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>, updates: Vec<Update>,
send: Sender<u32>, send: Sender<(TextureKind, u32)>,
recv: Receiver<u32>, recv: Receiver<(TextureKind, u32)>,
} }
pub enum TextureUpdate<'a> { pub enum TextureUpdate<'a> {
Push(&'a DynamicImage), Push(TextureKind, &'a DynamicImage),
Set(u32, &'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), Patch(u32, PatchRect, &'a DynamicImage),
Free(u32), Free(u32),
/// Added and freed before the renderer drained either update. It still has PushFree(TextureKind),
/// to push a slot to stay lined up with `images`; `Free` then empties it.
PushFree,
SetFree, SetFree,
} }
@@ -43,8 +64,8 @@ pub struct PatchRect {
} }
enum Update { enum Update {
Push(u32), Push(TextureKind, u32),
Set(u32), Set(TextureKind, u32),
Patch(u32, PatchRect), Patch(u32, PatchRect),
Free(u32), Free(u32),
} }
@@ -55,31 +76,54 @@ impl Textures {
Self { Self {
free: Vec::new(), free: Vec::new(),
images: Vec::new(), images: Vec::new(),
next_page_layer: 0,
updates: Vec::new(), updates: Vec::new(),
send, send,
recv, recv,
} }
} }
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle { pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
let image = image.into(); let image = image.into();
let size = image.dimensions().into(); let size = image.dimensions().into();
let kind = TextureKind::Image;
let slot = self.push(kind, image);
TextureHandle { TextureHandle {
slot: self.push(image), slot,
kind,
size, size,
counter: RefCounter::new(), counter: RefCounter::new(),
send: self.send.clone(), 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() { if let Some(i) = self.free.pop() {
self.images[i as usize] = Some(image); self.images[i as usize] = Some(image);
self.updates.push(Update::Set(i)); self.updates.push(Update::Set(kind, i));
i i
} else { } else {
let i = self.images.len() as u32; let i = self.images.len() as u32;
self.images.push(Some(image)); self.images.push(Some(image));
self.updates.push(Update::Push(i)); self.updates.push(Update::Push(kind, i));
i i
} }
} }
@@ -95,29 +139,30 @@ impl Textures {
self.updates.push(Update::Patch(handle.slot, rect)); 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) { 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.images[idx as usize] = None;
self.updates.push(Update::Free(idx)); self.updates.push(Update::Free(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); self.free.push(idx);
} }
} }
}
pub fn updates(&mut self) -> impl Iterator<Item = TextureUpdate<'_>> { pub fn updates(&mut self) -> impl Iterator<Item = TextureUpdate<'_>> {
self.updates.drain(..).map(|u| match u { 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() .as_ref()
.map(TextureUpdate::Push) .map(|img| TextureUpdate::Push(kind, img))
.unwrap_or(TextureUpdate::PushFree), .unwrap_or(TextureUpdate::PushFree(kind)),
Update::Set(i) => self.images[i as usize] Update::Set(kind, i) => self.images[i as usize]
.as_ref() .as_ref()
.map(|img| TextureUpdate::Set(i, img)) .map(|img| TextureUpdate::Set(kind, i, img))
.unwrap_or(TextureUpdate::SetFree), .unwrap_or(TextureUpdate::SetFree),
Update::Patch(i, rect) => self.images[i as usize] Update::Patch(i, rect) => self.images[i as usize]
.as_ref() .as_ref()
@@ -129,19 +174,36 @@ impl Textures {
} }
impl TextureHandle { impl TextureHandle {
/// Index into `Textures`, and into the renderer's parallel slots.
pub fn slot(&self) -> u32 {
self.slot
}
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
self.size 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 { impl Drop for TextureHandle {
fn drop(&mut self) { fn drop(&mut self) {
if self.counter.drop() { if self.counter.drop() {
let _ = self.send.send(self.slot); let _ = self.send.send((self.kind, self.slot));
} }
} }
} }
+38 -47
View File
@@ -1,11 +1,15 @@
use crate::{ use crate::{
PatchRect, PatchRect, TextureHandle, Textures,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
use image::RgbaImage; use image::RgbaImage;
use swash::scale::image::{Content, Image}; 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; pub(crate) const PAGE: u32 = 1024;
/// Transparent margin kept around every glyph, so that sampling one cannot /// Transparent margin kept around every glyph, so that sampling one cannot
@@ -36,7 +40,7 @@ pub struct GlyphEntry {
pub width: u32, pub width: u32,
pub height: u32, pub height: u32,
pub is_colored: bool, pub is_colored: bool,
/// Which atlas array layer this glyph is on. /// The atlas array layer this glyph's page occupies.
pub layer: u32, pub layer: u32,
} }
@@ -49,26 +53,18 @@ impl GlyphEntry {
} }
struct Page { struct Page {
image: RgbaImage, handle: TextureHandle,
x: u32, x: u32,
y: u32, y: u32,
shelf_height: 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)] #[derive(Default)]
pub struct GlyphAtlas { pub struct GlyphAtlas {
pages: Vec<Page>, pages: Vec<Page>,
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached /// `None` for a glyph that rasterised to nothing -- a space, say. Cached
/// too, so it is not re-rasterised on every layout. /// too, so it is not re-rasterised on every layout.
entries: HashMap<GlyphKey, Option<GlyphEntry>>, entries: HashMap<GlyphKey, Option<GlyphEntry>>,
uploads: Vec<PageUpload>,
} }
impl GlyphAtlas { impl GlyphAtlas {
@@ -76,7 +72,12 @@ impl GlyphAtlas {
self.entries.get(key).copied() 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 w = image.placement.width;
let h = image.placement.height; let h = image.placement.height;
if w == 0 || h == 0 { if w == 0 || h == 0 {
@@ -98,11 +99,23 @@ impl GlyphAtlas {
return None; return None;
} }
let upload = self.allocate(w, h); let (page_idx, x, y) = self.allocate(w, h, textures);
let PatchRect { x, y, .. } = upload.rect; let page = &self.pages[page_idx];
write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y);
self.uploads.push(upload);
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 scale = 1.0 / PAGE as f32;
let entry = GlyphEntry { let entry = GlyphEntry {
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale), uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
@@ -112,60 +125,38 @@ impl GlyphAtlas {
width: w, width: w,
height: h, height: h,
is_colored: matches!(image.content, Content::Color), is_colored: matches!(image.content, Content::Color),
layer: upload.layer, layer: page.handle.layer(),
}; };
self.entries.insert(key, Some(entry)); self.entries.insert(key, Some(entry));
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, textures: &mut Textures) -> (usize, u32, u32) {
fn allocate(&mut self, w: u32, h: u32) -> PageUpload {
let rect = |x, y| PatchRect {
x,
y,
width: w,
height: h,
};
if let Some((i, (x, y))) = self if let Some((i, (x, y))) = self
.pages .pages
.iter_mut() .iter_mut()
.enumerate() .enumerate()
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position))) .find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
{ {
return PageUpload { return (i, x, y);
layer: i as u32,
rect: rect(x, y),
};
} }
let handle = textures.add_page(RgbaImage::new(PAGE, PAGE));
self.pages.push(Page { self.pages.push(Page {
image: RgbaImage::new(PAGE, PAGE), handle,
x: PAD + w + PAD, x: PAD + w + PAD,
y: PAD, y: PAD,
shelf_height: h + PAD, shelf_height: h + PAD,
}); });
PageUpload { (self.pages.len() - 1, PAD, PAD)
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))
} }
pub fn insert_empty(&mut self, key: GlyphKey) { pub fn insert_empty(&mut self, key: GlyphKey) {
self.entries.insert(key, None); self.entries.insert(key, None);
} }
pub fn page_count(&self) -> u32 { pub fn page_count(&self) -> usize {
self.pages.len() as u32 self.pages.len()
} }
pub fn glyph_count(&self) -> usize { pub fn glyph_count(&self) -> usize {
+5 -1
View File
@@ -12,16 +12,20 @@ pub struct WindowUniform {
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PrimitiveInstance { pub struct PrimitiveInstance {
pub region: UiRegion, pub region: UiRegion,
pub binding: u32,
pub idx: u32,
pub mask_idx: MaskIdx, pub mask_idx: MaskIdx,
} }
impl PrimitiveInstance { impl PrimitiveInstance {
const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![ const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![
0 => Float32x2, 0 => Float32x2,
1 => Float32x2, 1 => Float32x2,
2 => Float32x2, 2 => Float32x2,
3 => Float32x2, 3 => Float32x2,
4 => Uint32, 4 => Uint32,
5 => Uint32,
6 => Uint32,
]; ];
pub fn desc() -> VertexBufferLayout<'static> { pub fn desc() -> VertexBufferLayout<'static> {
+242 -212
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
UiData, UiRenderState, UiData, UiRenderState,
render::{data::PrimitiveInstance, util::ArrBuf}, render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
use data::WindowUniform; use data::WindowUniform;
@@ -11,7 +11,6 @@ use wgpu::{
mod atlas; mod atlas;
mod data; mod data;
mod page;
mod primitive; mod primitive;
mod texture; mod texture;
mod util; mod util;
@@ -20,62 +19,64 @@ pub use atlas::*;
pub use data::{Mask, MaskIdx}; pub use data::{Mask, MaskIdx};
pub use primitive::*; pub use primitive::*;
const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
pub struct UiRenderNode { pub struct UiRenderNode {
shared_layout: BindGroupLayout, uniform_group: BindGroup,
shared_group: BindGroup, primitive_layout: BindGroupLayout,
format: TextureFormat, rsc_layout: BindGroupLayout,
rsc_group: BindGroup,
/// One per registered primitive, in id order. pipeline: RenderPipeline,
primitives: Vec<PrimitivePipeline>,
layers: HashMap<usize, RenderLayer>, layers: HashMap<usize, RenderLayer>,
active: Vec<usize>, active: Vec<usize>,
window_buffer: Buffer, window_buffer: Buffer,
textures: GpuTextures,
masks: ArrBuf<Mask>, masks: ArrBuf<Mask>,
} }
struct RenderLayer { 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>, instance: ArrBuf<PrimitiveInstance>,
data: ArrBuf<u8>, primitives: PrimitiveBuffers,
group: Option<BindGroup>, primitive_group: BindGroup,
/// What the primitive asked to keep per instance, if anything. /// A standalone image's instances, kept apart from `instance` because
bindings: Vec<u32>, /// 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 { impl UiRenderNode {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) { 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 { for i in &self.active {
let layer = &self.layers[i]; let layer = &self.layers[i];
for (id, list) in layer.primitives.iter().enumerate() { if layer.instance.len() == 0 && layer.image_instance.len() == 0 {
let Some(list) = list else { continue }; continue;
let Some(group) = &list.group else { continue }; }
let primitive = &self.primitives[id]; pass.set_bind_group(1, &layer.primitive_group, &[]);
pass.set_pipeline(&primitive.pipeline); if layer.instance.len() > 0 {
pass.set_bind_group(1, group, &[]); pass.set_bind_group(2, &self.rsc_group, &[]);
pass.set_vertex_buffer(0, list.instance.buffer.slice(..)); pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
primitive.render.draw( pass.draw(0..4, 0..layer.instance.len() as u32);
pass, }
ListDraw { // Images draw after this layer's rects and glyphs, one draw call
instances: list.instance.len() as u32, // each with its own bind group. That draws every image "on top"
bindings: &list.bindings, // 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,56 +88,82 @@ impl UiRenderNode {
ui: &mut UiData, ui: &mut UiData,
ui_render: &mut UiRenderState, 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(); 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); 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) { if let Some(inst) = ui_render.active.get_mut(&change.id) {
for h in &mut inst.primitives { 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; h.inst_idx = change.new;
break; break;
} }
} }
} }
} }
let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new); let rlayer = self.layers.entry(i).or_insert_with(|| {
if draws.updated { let primitives = PrimitiveBuffers::new(device);
let lists = draws.primitives(); let primitive_group =
// The zip would otherwise skip a list with no pipeline. Self::primitive_group(device, &self.primitive_layout, primitives.buffers());
assert!(lists.len() <= self.primitives.len()); RenderLayer {
rlayer.primitives.resize_with(lists.len(), || None); instance: ArrBuf::new(
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);
}
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(
device, device,
&self.shared_layout, BufferUsages::VERTEX | BufferUsages::COPY_DST,
&self.window_buffer, "instance",
&self.masks, ),
); primitives,
primitive_group,
image_instance: ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"image instance",
),
image_tex_indices: Vec::new(),
} }
});
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.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);
} }
} }
@@ -149,7 +176,12 @@ impl UiRenderNode {
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); 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 { let window_uniform = WindowUniform {
width: config.width as f32, width: config.width as f32,
height: config.height as f32, height: config.height as f32,
@@ -160,73 +192,65 @@ impl UiRenderNode {
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, 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( let masks = ArrBuf::new(
device, device,
BufferUsages::STORAGE | BufferUsages::COPY_DST, BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks", "ui masks",
); );
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
Self { let rsc_layout = Self::rsc_layout(device);
shared_layout, let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
shared_group,
format: config.format,
primitives: Vec::new(),
window_buffer,
layers: HashMap::default(),
active: Vec::new(),
masks,
}
}
/// Compiles a pipeline for every primitive registered since the last call. let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
/// Sources only ever arrive at the end, so an id keeps its pipeline. label: Some("UI Shape Pipeline Layout"),
fn build_pipelines(&mut self, device: &Device, queue: &Queue, registry: &PrimitiveRegistry) { bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
for source in &registry.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, immediate_size: 0,
}); });
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label); let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
self.primitives.push(PrimitivePipeline { label: Some("UI Shape Pipeline"),
data_layout, layout: Some(&pipeline_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()),
});
device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some(label),
layout: Some(layout),
vertex: VertexState { vertex: VertexState {
module: &module, module: &shader,
entry_point: Some("vs_main"), entry_point: Some("vs_main"),
buffers: &[Some(PrimitiveInstance::desc())], buffers: &[PrimitiveInstance::desc()],
compilation_options: Default::default(), compilation_options: Default::default(),
}, },
fragment: Some(FragmentState { fragment: Some(FragmentState {
module: &module, module: &shader,
entry_point: Some("fs_main"), entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState { targets: &[Some(ColorTargetState {
format, format: config.format,
blend: Some(BlendState::ALPHA_BLENDING), blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL, write_mask: ColorWrites::ALL,
})], })],
@@ -249,42 +273,107 @@ impl UiRenderNode {
}, },
multiview_mask: None, multiview_mask: None,
cache: 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 primitive_group(
fn shared_layout(device: &Device) -> BindGroupLayout { 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 { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[
BindGroupLayoutEntry { BindGroupLayoutEntry {
binding: 0, binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer { ty: BindingType::Texture {
ty: BufferBindingType::Uniform, sample_type: TextureSampleType::Float { filterable: false },
has_dynamic_offset: false, view_dimension: TextureViewDimension::D2Array,
min_binding_size: BufferSize::new(size_of::<WindowUniform>() as u64), multisampled: false,
}, },
count: None, count: None,
}, },
BindGroupLayoutEntry { BindGroupLayoutEntry {
binding: 1, binding: 1,
visibility: ShaderStages::FRAGMENT, 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: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true }, ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false, has_dynamic_offset: false,
min_binding_size: BufferSize::new(size_of::<Mask>() as u64), min_binding_size: None,
}, },
count: 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, device: &Device,
layout: &BindGroupLayout, layout: &BindGroupLayout,
window: &Buffer, tex_manager: &GpuTextures,
masks: &ArrBuf<Mask>, masks: &ArrBuf<Mask>,
) -> BindGroup { ) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor { device.create_bind_group(&BindGroupDescriptor {
@@ -292,85 +381,26 @@ impl UiRenderNode {
entries: &[ entries: &[
BindGroupEntry { BindGroupEntry {
binding: 0, binding: 0,
resource: window.as_entire_binding(), resource: BindingResource::TextureView(tex_manager.array_view()),
}, },
BindGroupEntry { BindGroupEntry {
binding: 1, 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(), 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 pub fn view_count(&self) -> usize {
/// stated, so "is the buffer big enough for one entry?" is answered when self.textures.view_count()
/// 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"),
}));
}
} }
} }
-156
View File
@@ -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: &[],
})
}
+331 -297
View File
@@ -1,247 +1,144 @@
use std::{any::TypeId, marker::PhantomData}; use std::ops::{Deref, DerefMut};
use crate::{ use crate::{
Color, TextureHandle, UiData, UiRegion, WidgetId, Color, UiRegion, WidgetId,
render::{ render::{
ArrBuf,
data::{MaskIdx, PrimitiveInstance}, data::{MaskIdx, PrimitiveInstance},
page::GlyphRender,
texture::ImageRender,
}, },
util::{HashMap, Vec2}, util::Vec2,
}; };
use bytemuck::Pod; use bytemuck::Pod;
use wgpu::{BindGroupLayout, Device, Queue, RenderPass}; use wgpu::*;
/// One instance of a primitive, laid out as the struct its shader reads. pub struct Primitives {
///
/// 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 {
instances: Vec<PrimitiveInstance>, instances: Vec<PrimitiveInstance>,
/// The widget each instance belongs to, for renumbering its handles.
assoc: Vec<WidgetId>, assoc: Vec<WidgetId>,
data: PrimitiveData,
free: Vec<usize>, 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 { /// Standalone images, kept apart from `instances` because each one draws
fn new<P: Primitive>() -> Self { /// with its own bind group rather than sharing the layer's one instanced
Self { /// draw. `idx` on each `PrimitiveInstance` here is the texture's slot in
instances: Vec::new(), /// `Textures`/`GpuTextures`, not an index into `data`: a bind group has
assoc: Vec::new(), /// already picked the texture, so there is nothing left to look up
free: Vec::new(), /// per-instance and no per-image entry in `data` at all.
data: Vec::new(), images: Vec<PrimitiveInstance>,
stride: size_of::<P>(), 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, pub updated: bool,
} }
impl Default for LayerDraws { impl Default for Primitives {
fn default() -> Self { fn default() -> Self {
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, 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>( pub fn write<P: Primitive>(
&mut self, &mut self,
layer: usize, layer: usize,
PrimitiveInst { PrimitiveInst {
kind,
id, id,
primitive, primitive,
region, region,
@@ -249,67 +146,151 @@ impl LayerDraws {
}: PrimitiveInst<P>, }: PrimitiveInst<P>,
) -> PrimitiveHandle { ) -> PrimitiveHandle {
self.updated = true; self.updated = true;
// Grown on first use rather than sized from the registry, which a let vec = P::vec(&mut self.data);
// layer cannot see. let i = vec.add(primitive);
if self.primitives.len() <= kind.id as usize { let inst = PrimitiveInstance {
self.primitives.resize_with(kind.id as usize + 1, || None); 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)
} }
let inst_idx = self.primitives[kind.id as usize]
.get_or_insert_with(InstanceList::new::<P>) /// Writes an image instance directly -- there is no `Primitive` impl for
.push( /// it to go through `write`, since it has nowhere in `PrimitiveData` to
id, /// put a per-instance entry. `texture_idx` is the slot the bind group at
PrimitiveInstance { region, mask_idx }, /// draw time is chosen from, carried in the otherwise-unused `idx` field.
bytemuck::bytes_of(&primitive), 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 { PrimitiveHandle {
layer, layer,
kind: kind.id, inst_idx: inst_i,
inst_idx, data_idx: 0,
binding: IMAGE_BINDING,
} }
} }
pub fn primitives(&self) -> &[Option<InstanceList>] { pub fn image_instances(&self) -> &Vec<PrimitiveInstance> {
&self.primitives &self.images
} }
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> { /// returns (old index, new index) for both lists this layer keeps --
self.primitives /// `PrimitiveChange::is_image` says which, since the two have separate
.iter_mut() /// index spaces and `old`/`new` alone would collide between them.
.enumerate() ///
.filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?))) /// Both lists free with `swap_remove`, so a layer's draw order was
.flat_map(|(kind, list)| list.apply_free(kind)) /// 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 { pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self.updated = true; 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 { pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
self.updated = true; 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 {
/// A handle is only ever made by `write`, which is what created the list. &mut self.instances[h.inst_idx].region
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 struct PrimitiveChange {
pub id: WidgetId, pub id: WidgetId,
/// Which registered primitive's list moved, since they index separately. /// Which of `Primitives::instances`/`Primitives::images` this change
pub kind: u32, /// 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 old: usize,
pub new: usize, pub new: usize,
} }
@@ -317,12 +298,29 @@ pub struct PrimitiveChange {
#[derive(Debug)] #[derive(Debug)]
pub struct PrimitiveHandle { pub struct PrimitiveHandle {
pub layer: usize, pub layer: usize,
pub kind: u32,
pub inst_idx: usize, 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)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone)]
pub struct RectPrimitive { pub struct RectPrimitive {
pub color: Color<u8>, pub color: Color<u8>,
pub radius: f32, pub radius: f32,
@@ -330,10 +328,6 @@ pub struct RectPrimitive {
pub inner_radius: f32, pub inner_radius: f32,
} }
impl Primitive for RectPrimitive {
const WGSL: &'static str = include_str!("shader/rect.wgsl");
}
impl RectPrimitive { impl RectPrimitive {
pub fn color(color: Color<u8>) -> Self { pub fn color(color: Color<u8>) -> Self {
Self { Self {
@@ -345,51 +339,91 @@ impl RectPrimitive {
} }
} }
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph /// One glyph, drawn as a sub-rectangle of the glyph atlas array.
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects. ///
#[repr(C, align(8))] /// `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)] #[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive { pub struct GlyphPrimitive {
pub uv_min: Vec2, pub uv_min: Vec2,
pub uv_max: 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 layer: u32,
pub color: Color<u8>, pub color: Color<u8>,
pub flags: u32, 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 impl GlyphPrimitive {
// is how WGSL lays the struct out. /// The only constructor, since `_pad` is private: callers outside this
unsafe impl bytemuck::Pod for GlyphPrimitive {} /// module cannot write the struct literal.
unsafe impl bytemuck::Zeroable for GlyphPrimitive {} pub fn new(uv_min: Vec2, uv_max: Vec2, layer: u32, color: Color<u8>, flags: u32) -> Self {
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 {
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
}
}
+210
View File
@@ -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;
}
-33
View File
@@ -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);
}
-96
View File
@@ -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;
}
-39
View File
@@ -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;
}
-10
View File
@@ -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));
}
+385 -169
View File
@@ -1,112 +1,289 @@
use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage}; use image::{DynamicImage, EncodableLayout, GenericImageView};
use wgpu::{util::DeviceExt, *}; use wgpu::{util::DeviceExt, *};
use crate::{ use crate::{Mask, PatchRect, TextureKind, TextureUpdate, Textures, render::util::ArrBuf};
PatchRect, TextureUpdate, Textures, UiData,
render::{
TexturePrimitive,
primitive::{ListDraw, PrimitiveRender},
},
};
/// Draws standalone images, which it owns. Each is its own texture, so each use super::atlas::PAGE;
/// instance binds its own and is a draw of its own.
pub struct ImageRender {
textures: GpuTextures,
layout: BindGroupLayout,
sampler: Sampler,
}
impl ImageRender { /// What one texture slot is, GPU-side. Parallel to `Textures`' own slot
pub fn new(device: &Device, queue: &Queue) -> Self { /// numbering (`TextureKind`'s `Image`/`Page`), so a slot's index means the
Self { /// same thing on both sides without a second map to keep in sync.
textures: GpuTextures::new(device, queue), enum Slot {
layout: sampled_layout(device, TextureViewDimension::D2, "ui image"), /// A slot that was freed, or pushed and freed within the same batch
sampler: default_sampler(device), /// 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`.
impl PrimitiveRender for ImageRender { Page(u32),
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>>,
} }
struct ImageGpu { struct ImageGpu {
/// Kept for `patch`, which needs the texture rather than the view. /// Kept because a masks or atlas-array rebuild has to build a new bind
texture: Texture, /// group from it. The `Texture` it came from is not kept: a `TextureView`
group: BindGroup, /// 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 { impl GpuTextures {
pub fn new(device: &Device, queue: &Queue) -> Self { /// Applies queued `Textures` updates, then reports whether the *main*
Self { /// bind group (the one rects and glyphs draw with) needs rebuilding --
device: device.clone(), /// true when the atlas array was recreated (its view identity changed)
queue: queue.clone(), /// or the masks buffer was, since both are bound there. Pushing or
slots: Vec::new(), /// 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() { for update in textures.updates() {
match update { match update {
TextureUpdate::Push(image) => { TextureUpdate::Push(kind, image) => {
let image = self.create(image, layout, sampler); rebuild_main |= self.push(kind, image, rsc_layout, masks);
self.slots.push(Some(image));
} }
TextureUpdate::Set(i, image) => { TextureUpdate::Set(kind, i, image) => {
let image = self.create(image, layout, sampler); rebuild_main |= self.set(kind, i, image, rsc_layout, masks);
self.slots[i as usize] = Some(image);
} }
// 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::Patch(i, rect, image) => self.patch(i, rect, image),
TextureUpdate::PushFree => self.slots.push(None),
TextureUpdate::SetFree => {} 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> { fn free(&mut self, i: u32) {
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group) 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, &self,
image: &DynamicImage, image: &DynamicImage,
layout: &BindGroupLayout, rsc_layout: &BindGroupLayout,
sampler: &Sampler, masks: &ArrBuf<Mask>,
) -> ImageGpu { ) -> ImageGpu {
let rgba = image.to_rgba8(); let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions(); let (width, height) = rgba.dimensions();
@@ -130,114 +307,153 @@ impl GpuTextures {
rgba.as_bytes(), rgba.as_bytes(),
); );
let view = texture.create_view(&TextureViewDescriptor::default()); let view = texture.create_view(&TextureViewDescriptor::default());
let group = sampled_group(&self.device, layout, &view, sampler, "ui image"); let bind_group = Self::make_image_bind_group(
ImageGpu { texture, group } &self.device,
} rsc_layout,
&self.array_view,
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) { &view,
let Some(Some(slot)) = self.slots.get(i as usize) else { &self.sampler,
return; masks,
};
let dst = TexelCopyTextureInfo {
texture: &slot.texture,
mip_level: 0,
origin: Origin3d {
x: rect.x,
y: rect.y,
z: 0,
},
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 });
}
}
}
}
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,
},
); );
ImageGpu { view, bind_group }
} }
/// What a primitive that samples binds: a texture, and the sampler that reads /// Builds group 2 for one standalone image: the shared atlas array, this
/// it. /// image's own view, the shared sampler, and the shared masks buffer --
pub fn sampled_group( /// the same layout the main draw uses with a null view in the image slot.
fn make_image_bind_group(
device: &Device, device: &Device,
layout: &BindGroupLayout, rsc_layout: &BindGroupLayout,
view: &TextureView, array_view: &TextureView,
image_view: &TextureView,
sampler: &Sampler, sampler: &Sampler,
label: &'static str, masks: &ArrBuf<Mask>,
) -> BindGroup { ) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor { device.create_bind_group(&BindGroupDescriptor {
layout, layout: rsc_layout,
entries: &[ entries: &[
BindGroupEntry { BindGroupEntry {
binding: 0, binding: 0,
resource: BindingResource::TextureView(view), resource: BindingResource::TextureView(array_view),
}, },
BindGroupEntry { BindGroupEntry {
binding: 1, binding: 1,
resource: BindingResource::TextureView(image_view),
},
BindGroupEntry {
binding: 2,
resource: BindingResource::Sampler(sampler), resource: BindingResource::Sampler(sampler),
}, },
BindGroupEntry {
binding: 3,
resource: masks.buffer.as_entire_binding(),
},
], ],
label: Some(label), label: Some("ui rsc image"),
}) })
} }
/// The layout for one of those. The dimension differs -- the atlas is an fn create_array_texture(device: &Device, capacity: u32) -> Texture {
/// array of pages and an image is not -- and nothing else does. device.create_texture(&TextureDescriptor {
pub fn sampled_layout( label: Some("glyph atlas array"),
device: &Device, size: Extent3d {
dimension: TextureViewDimension, width: PAGE,
label: &'static str, height: PAGE,
) -> BindGroupLayout { depth_or_array_layers: capacity,
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, mip_level_count: 1,
}, sample_count: 1,
BindGroupLayoutEntry { dimension: TextureDimension::D2,
binding: 1, format: TextureFormat::Rgba8Unorm,
visibility: ShaderStages::FRAGMENT, usage: TextureUsages::TEXTURE_BINDING
ty: BindingType::Sampler(SamplerBindingType::NonFiltering), | TextureUsages::COPY_DST
count: None, | TextureUsages::COPY_SRC,
}, view_formats: &[],
],
label: Some(label),
}) })
} }
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 null_texture_view(device: &Device) -> TextureView {
device
.create_texture(&TextureDescriptor {
label: Some("null"),
size: Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
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 { pub fn default_sampler(device: &Device) -> Sampler {
device.create_sampler(&SamplerDescriptor::default()) device.create_sampler(&SamplerDescriptor::default())
} }
+7 -6
View File
@@ -21,8 +21,9 @@ impl<T: Pod> ArrBuf<T> {
_pd: PhantomData, _pd: PhantomData,
} }
} }
/// Returns whether the `Buffer` was recreated, which stales any cached /// Returns whether the underlying `Buffer` was recreated -- a caller that
/// `BindGroup` holding it. /// 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 { pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
let resized = self.len != data.len(); let resized = self.len != data.len();
if resized { if resized {
@@ -33,13 +34,9 @@ impl<T: Pod> ArrBuf<T> {
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data)); queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
resized resized
} }
pub fn len(&self) -> usize {
self.len
}
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer { fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64; let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) { 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); size = size.max(std::mem::size_of::<T>() as u64);
} }
device.create_buffer(&BufferDescriptor { device.create_buffer(&BufferDescriptor {
@@ -49,4 +46,8 @@ impl<T: Pod> ArrBuf<T> {
usage, usage,
}) })
} }
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.len
}
} }
+2 -6
View File
@@ -1,6 +1,4 @@
use crate::{ use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena};
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
};
mod active; mod active;
mod cache; mod cache;
@@ -9,15 +7,13 @@ mod render_state;
mod size; mod size;
pub use active::*; pub use active::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::Painter;
pub use render_state::*; pub use render_state::*;
pub use size::*; pub use size::*;
#[derive(Default)] #[derive(Default)]
pub struct UiData { pub struct UiData {
pub widgets: Widgets, pub widgets: Widgets,
/// Every primitive this ui can draw.
pub primitives: PrimitiveRegistry,
pub textures: Textures, pub textures: Textures,
pub text: TextData, pub text: TextData,
pub masks: TrackedArena<Mask, u32>, pub masks: TrackedArena<Mask, u32>,
+41 -56
View File
@@ -1,10 +1,7 @@
use crate::{ use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData, Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{ render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
},
util::Vec2, util::Vec2,
}; };
@@ -24,26 +21,15 @@ pub struct Painter<'a> {
impl<'a> Painter<'a> { impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) { 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( let h = self.state.layers.write(
self.layer, self.layer,
PrimitiveInst { PrimitiveInst {
kind,
id: self.id, id: self.id,
primitive, primitive,
region, region,
mask_idx: self.mask, mask_idx: self.mask,
}, },
); );
self.push_primitive(h);
}
fn push_primitive(&mut self, h: PrimitiveHandle) {
if self.mask != MaskIdx::NONE { if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy: // TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask); self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -52,13 +38,11 @@ impl<'a> Painter<'a> {
} }
/// Writes a primitive to be rendered /// Writes a primitive to be rendered
pub fn primitive(&mut self, primitive: impl PrimitiveLike) { pub fn primitive<P: Primitive>(&mut self, primitive: P) {
let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, self.region) self.primitive_at(primitive, self.region)
} }
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) { pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, region.within(&self.region)); 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( pub fn render_text(
&mut self, &mut self,
buffer: &mut TextBuffer, buffer: &mut TextBuffer,
@@ -98,12 +111,10 @@ impl<'a> Painter<'a> {
width: Option<f32>, width: Option<f32>,
) -> RenderedText { ) -> RenderedText {
let ui = self.rsc.ui_mut(); 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) { pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() { for glyph in text.glyphs.iter() {
let mut region = origin; let mut region = origin;
region.x.end = region.x.start; region.x.end = region.x.start;
@@ -111,15 +122,14 @@ impl<'a> Painter<'a> {
let mut region = region.offset(UiVec2::abs(glyph.offset)); let mut region = region.offset(UiVec2::abs(glyph.offset));
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32); 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); region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
self.write( self.primitive_at(
kind, GlyphPrimitive::new(
GlyphPrimitive { glyph.entry.uv_min,
uv_min: glyph.entry.uv_min, glyph.entry.uv_max,
uv_max: glyph.entry.uv_max, glyph.entry.layer,
layer: glyph.entry.layer, text.color,
color: text.color, glyph.entry.flags(),
flags: glyph.entry.flags(), ),
},
region, region,
); );
} }
@@ -172,28 +182,3 @@ impl<'a> Painter<'a> {
self.state.size_ctx(self.id, self.region.size(), self.rsc) 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
View File
@@ -1,13 +1,13 @@
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget, ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx,
UiRegion, UiRsc, UiVec2, WidgetId, Widgets, StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
ui::cache::Cache, ui::cache::Cache,
util::{HashMap, HashSet, Vec2, forget_ref}, util::{HashMap, HashSet, Vec2, forget_ref},
}; };
pub struct UiRenderState { pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>, pub active: HashMap<WidgetId, ActiveData>,
pub layers: DrawLayers, pub layers: PrimitiveLayers,
pub(super) output_size: Vec2, pub(super) output_size: Vec2,
pub cache: Cache, pub cache: Cache,
@@ -243,14 +243,14 @@ impl UiRenderState {
} }
pub fn debug_layers(&self) { 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 indent = " ".repeat(depth * 2);
let counts: Vec<String> = draws let len = primitives.instances().len();
.primitives() print!("{indent}{idx}: {len} primitives");
.iter() if len >= 1 {
.map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string()) print!(" ({})", primitives.instances()[0].binding);
.collect(); }
println!("{indent}{idx}: [{}]", counts.join(", ")); println!();
} }
} }
@@ -308,6 +308,7 @@ impl UiRenderState {
source, source,
cache: &mut self.cache, cache: &mut self.cache,
text: &mut ui.text, text: &mut ui.text,
textures: &mut ui.textures,
widgets: &ui.widgets, widgets: &ui.widgets,
outer, outer,
output_size: self.output_size, output_size: self.output_size,
+5 -3
View File
@@ -1,10 +1,11 @@
use crate::{ use crate::{
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2, Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2, UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
}; };
pub struct SizeCtx<'a> { pub struct SizeCtx<'a> {
pub text: &'a mut TextData, pub text: &'a mut TextData,
pub textures: &'a mut Textures,
pub(super) source: WidgetId, pub(super) source: WidgetId,
pub(super) widgets: &'a Widgets, pub(super) widgets: &'a Widgets,
pub(super) cache: &'a mut Cache, pub(super) cache: &'a mut Cache,
@@ -32,6 +33,7 @@ impl SizeCtx<'_> {
.get_dyn_dynamic(id) .get_dyn_dynamic(id)
.desired_len::<A>(&mut SizeCtx { .desired_len::<A>(&mut SizeCtx {
text: self.text, text: self.text,
textures: self.textures,
source: self.source, source: self.source,
widgets: self.widgets, widgets: self.widgets,
cache: self.cache, cache: self.cache,
@@ -80,7 +82,7 @@ impl SizeCtx<'_> {
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, width: Option<f32>,
) -> RenderedText { ) -> RenderedText {
self.text.render(buffer, attrs, width) self.text.render(buffer, attrs, width, self.textures)
} }
pub fn label(&self, id: WidgetId) -> &String { pub fn label(&self, id: WidgetId) -> &String {
+2 -2
View File
@@ -212,10 +212,10 @@ impl DefaultAppState for Client {
render: &mut UiRenderState, render: &mut UiRenderState,
) { ) {
let new = format!( let new = format!(
"widgets: {}\nactive: {}\ntextures: {}", "widgets: {}\nactive: {}\nviews: {}",
rsc.widgets().len(), rsc.widgets().len(),
render.active_widgets(), render.active_widgets(),
rsc.ui().textures.count(), self.ui_state.renderer.ui.view_count(),
); );
if new != *rsc.widgets()[self.info].content { if new != *rsc.widgets()[self.info].content {
*rsc.widgets_mut()[self.info].content = new; *rsc.widgets_mut()[self.info].content = new;
+5 -1
View File
@@ -1,6 +1,10 @@
use crate::prelude::*; use crate::prelude::*;
use arboard::Clipboard; use arboard::Clipboard;
use std::{marker::PhantomData, sync::Arc, time::Instant}; use std::{
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use winit::{ use winit::{
event::{Ime, WindowEvent}, event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoopProxy},
+11 -21
View File
@@ -22,20 +22,7 @@ impl UiRenderer {
} }
pub fn draw(&mut self) { pub fn draw(&mut self) {
let output = match self.surface.get_current_texture() { let output = self.surface.get_current_texture().unwrap();
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 view = output let view = output
.texture .texture
.create_view(&TextureViewDescriptor::default()); .create_view(&TextureViewDescriptor::default());
@@ -59,7 +46,7 @@ impl UiRenderer {
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
self.window.pre_present_notify(); self.window.pre_present_notify();
self.queue.present(output); output.present();
} }
pub fn resize(&mut self, size: &PhysicalSize<u32>) { pub fn resize(&mut self, size: &PhysicalSize<u32>) {
@@ -78,10 +65,9 @@ impl UiRenderer {
pub fn new(window: Arc<Window>) -> Self { pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size(); let size = window.inner_size();
let instance = Instance::new(InstanceDescriptor { let instance = Instance::new(&InstanceDescriptor {
backends: Backends::PRIMARY, backends: Backends::PRIMARY,
display: Some(Box::new(window.clone())), ..Default::default()
..InstanceDescriptor::new_without_display_handle()
}); });
let surface = instance let surface = instance
@@ -93,11 +79,16 @@ impl UiRenderer {
power_preference: PowerPreference::default(), power_preference: PowerPreference::default(),
compatible_surface: Some(&surface), compatible_surface: Some(&surface),
force_fallback_adapter: false, force_fallback_adapter: false,
apply_limit_buckets: false,
}) })
.block_on() .block_on()
.expect("Could not get adapter!"); .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 let (device, queue) = adapter
.request_device(&DeviceDescriptor { .request_device(&DeviceDescriptor {
required_limits: Limits { required_limits: Limits {
@@ -120,7 +111,6 @@ impl UiRenderer {
let config = SurfaceConfiguration { let config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT, usage: TextureUsages::RENDER_ATTACHMENT,
format: surface_format, format: surface_format,
color_space: SurfaceColorSpace::Auto,
width: size.width, width: size.width,
height: size.height, height: size.height,
present_mode: PresentMode::AutoVsync, present_mode: PresentMode::AutoVsync,
@@ -133,7 +123,7 @@ impl UiRenderer {
let encoder = Self::create_encoder(&device); let encoder = Self::create_encoder(&device);
let ui = UiRenderNode::new(&device, &config); let ui = UiRenderNode::new(&device, &queue, &config);
Self { Self {
surface, surface,
+1 -1
View File
@@ -7,7 +7,7 @@ pub struct Image {
impl Widget for Image { impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
painter.primitive(&self.handle); painter.texture(&self.handle);
} }
fn desired_width(&mut self, _: &mut SizeCtx) -> Len { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
+2 -2
View File
@@ -4,7 +4,7 @@ mod max_size;
mod offset; mod offset;
mod pad; mod pad;
mod scroll; mod scroll;
mod set_size; mod sized;
mod span; mod span;
mod stack; mod stack;
@@ -14,6 +14,6 @@ pub use max_size::*;
pub use offset::*; pub use offset::*;
pub use pad::*; pub use pad::*;
pub use scroll::*; pub use scroll::*;
pub use set_size::*; pub use sized::*;
pub use span::*; pub use span::*;
pub use stack::*; pub use stack::*;
@@ -1,12 +1,12 @@
use crate::prelude::*; use crate::prelude::*;
pub struct SetSize { pub struct Sized {
pub inner: StrongWidget, pub inner: StrongWidget,
pub x: Option<Len>, pub x: Option<Len>,
pub y: Option<Len>, pub y: Option<Len>,
} }
impl SetSize { impl Sized {
fn apply_to_outer(&self, ctx: &mut SizeCtx) { fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x { if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest()); ctx.outer.x.select_len(x.apply_rest());
@@ -17,7 +17,7 @@ impl SetSize {
} }
} }
impl Widget for SetSize { impl Widget for Sized {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner); painter.widget(&self.inner);
} }
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::Unsize; use std::marker::{Sized, Unsize};
pub struct WidgetPtr { pub struct WidgetPtr {
pub inner: Option<StrongWidget>, pub inner: Option<StrongWidget>,
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::PhantomData; use std::marker::{PhantomData, Sized};
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> { pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
pub content: String, pub content: String,
+6 -6
View File
@@ -31,9 +31,9 @@ widget_trait! {
} }
} }
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> { fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> {
let size = size.into(); let size = size.into();
move |state| SetSize { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
x: Some(size.x), x: Some(size.x),
y: Some(size.y), y: Some(size.y),
@@ -58,18 +58,18 @@ widget_trait! {
} }
} }
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> { fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into(); let len = len.into();
move |state| SetSize { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
x: Some(len), x: Some(len),
y: None, y: None,
} }
} }
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> { fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into(); let len = len.into();
move |state| SetSize { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
x: None, x: None,
y: Some(len), y: Some(len),
-205
View File
@@ -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
);
}
}