Compare commits

..
Author SHA1 Message Date
iris c4b74f2367 Drop the prelude-bounds test
The rename is the guarantee; there is no way back to a name that is not
there any more.
2026-09-13 20:14:43 -04:00
irisandClaude Opus 5 f312db60c2 Rename the Sized widget to SetSize
It shadowed the marker trait, so a `?Sized` bound in any crate that imports
the prelude failed to resolve -- a compile error in someone else's code that
nothing here would have caught. Three files inside iris already imported
`std::marker::Sized` to get out from under it; they no longer need to.

`SetSize` rather than `FixedSize` because the size it sets need not be fixed:
`width(rest(2))` and `width(rel(0.5))` build the same widget. It pairs with
the `MaxSize` beside it -- one sets a length, the other caps it.

`tests/prelude_bounds.rs` is a compile-level guard: it fails to build if the
prelude shadows `Sized` again. The pad tab of the tabs example, which is what
uses `sized` and the flexible widths, renders pixel-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 19:34:52 -04:00
iris-aiandiris 00d2230b84 Build on wgpu 30 (#13)
Two majors, and the renderer is under everything else left to extract -- so it goes before the slices that would otherwise be written against wgpu 28 and then again against 30. `image` 0.25.6 -> 0.25.10 rides along. `winit` stays on 0.30.12, since 0.31 is only a prerelease and nothing here needs it; `parley` 0.11.1 is current.

What the API asked for, beyond the version:

- **An instance takes the display it will present on**, and GLES on Wayland needs it, so the window the surface is made from is handed over with it. That one matters for Android rather than for this machine.
- **`get_current_texture` returns a status rather than a `Result`**, which replaced an `unwrap` that would have panicked on a resize or an occluded window: reconfigure when the surface is outdated, lost or suboptimal, and skip the frame when there is nothing to draw into.
- **Presenting moved to the queue**, still after `pre_present_notify`.
- **Bind group and vertex buffer layouts are sparse**, so each slot states `Some(layout)`.

Verified the same way as #11: the tabs example with two runtime-added images, an image alone in a layer, and glyphs from a four-page atlas all render identically. `tests/draw_cost.rs` gives 33.6/167/587/2855 us per frame at 8/64/256/1024 layers, against 33.3/161/588/2903 on wgpu 28 -- no change.

---------

Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: iris/iris#13
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
2026-09-13 19:07:47 -04:00
iris-aiandiris b234497d21 Draw the glyph atlas as an array texture and images with their own bind groups + primitive rendering overhaul
Replaces the bindless `binding_array<texture_2d<f32>>` the renderer bound every texture through. That array needs `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack, so the old shape did not run there at all.

The two things being bound want opposite treatment, so they are now split:

- **Glyph atlas pages become layers of one `texture_2d_array`.** A glyph primitive carries a `layer` instead of a view/sampler index pair. A layer index is an ordinary sampling operand, so this needs nothing beyond plain Vulkan 1.0 / GLES. Growing the atlas recreates the array with headroom and `copy_texture_to_texture`s the old layers across, no readback.
- **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`: the bind group has already picked the texture.

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

Two notes on judgement calls, since this slice was rebuilt on top of `main` rather than transplanted:

- The source version renamed `GlyphEntry::is_colored` to `is_color` and added a second `IS_COLOR` flag constant beside the existing `GlyphEntry::IS_COLORED`. Both dropped: #10's naming and its `flags()` are kept, and UVs stay `Vec2` rather than going back to `[f32; 2]`.
- `ImageGpu` no longer holds the `Texture` behind its view, which removes an `#[allow(dead_code)]`. A `TextureView` keeps its own reference to the texture, checked by rendering rather than assumed -- see below.

### Verification

```
cargo fmt --all --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
```

All clean; the 4 text-edit tests pass. The only clippy output is the pre-existing future-incompatibility notice about `naga`/`wgpu`/`winit`.

Because this is a rendering change, it was also run for real rather than only compiled. The `tabs` example was rendered on this machine's GPU -- Venus onto an RX 7900 XT, confirmed from the loaded ICD (`libvulkan_virtio.so` on `/dev/dri/renderD128`) rather than assumed, since a failed Vulkan init here silently falls back to llvmpipe and would make the screenshots meaningless.

Screenshots before and after the change are **byte-identical** (same md5) in two scenes: the default tab, which exercises text (the atlas path) and rects, and the image tab with a standalone image pushed at startup, which exercises the per-image bind group. The image-tab scene needed a temporary local edit to the example to push the image without a click; that edit is not part of this branch. The same comparison, re-run after dropping the `Texture` field, is still byte-identical -- which is the check that the view alone keeps it alive.

---------

Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: iris/iris#11
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
2026-09-13 18:56:59 -04:00
31 changed files with 1635 additions and 1130 deletions

No files matched your search

Generated
+203 -229
View File
@@ -234,18 +234,18 @@ dependencies = [
[[package]]
name = "bit-set"
version = "0.8.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
[[package]]
name = "bit_field"
@@ -274,12 +274,6 @@ dependencies = [
"core2",
]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "block2"
version = "0.5.1"
@@ -289,6 +283,15 @@ dependencies = [
"objc2 0.5.2",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2 0.6.4",
]
[[package]]
name = "built"
version = "0.8.0"
@@ -391,9 +394,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "clipboard-win"
@@ -406,9 +409,9 @@ dependencies = [
[[package]]
name = "codespan-reporting"
version = "0.12.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"termcolor",
@@ -450,16 +453,6 @@ dependencies = [
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -473,8 +466,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"core-foundation",
"core-graphics-types",
"foreign-types",
"libc",
]
@@ -486,18 +479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.10.0",
"core-foundation 0.10.1",
"core-foundation",
"libc",
]
@@ -739,15 +721,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "font-types"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5"
dependencies = [
"bytemuck",
]
[[package]]
name = "font-types"
version = "0.12.4"
@@ -771,7 +744,7 @@ dependencies = [
"objc2-core-text",
"objc2-foundation 0.3.2",
"parlance",
"read-fonts 0.41.0",
"read-fonts",
"roxmltree",
"smallvec",
"windows",
@@ -806,6 +779,30 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "fxhash"
version = "0.2.1"
@@ -860,9 +857,9 @@ dependencies = [
[[package]]
name = "glow"
version = "0.16.0"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5"
dependencies = [
"js-sys",
"slotmap",
@@ -893,26 +890,6 @@ dependencies = [
"windows",
]
[[package]]
name = "gpu-descriptor"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
dependencies = [
"bitflags 2.10.0",
"gpu-descriptor-types",
"hashbrown 0.15.5",
]
[[package]]
name = "gpu-descriptor-types"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "half"
version = "2.7.1"
@@ -933,7 +910,7 @@ checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9"
dependencies = [
"bitflags 2.10.0",
"bytemuck",
"read-fonts 0.41.0",
"read-fonts",
"smallvec",
]
@@ -972,12 +949,6 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hexf-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "icu_collections"
version = "2.3.0"
@@ -1108,9 +1079,9 @@ checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad"
[[package]]
name = "image"
version = "0.25.9"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
@@ -1126,8 +1097,8 @@ dependencies = [
"rayon",
"rgb",
"tiff",
"zune-core 0.5.0",
"zune-jpeg 0.5.8",
"zune-core",
"zune-jpeg",
]
[[package]]
@@ -1247,11 +1218,12 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.83"
version = "0.3.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
dependencies = [
"once_cell",
"cfg-if",
"futures-util",
"wasm-bindgen",
]
@@ -1280,9 +1252,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "libc"
version = "0.2.179"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libfuzzer-sys"
@@ -1375,15 +1347,6 @@ dependencies = [
"imgref",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "maybe-rayon"
version = "0.1.1"
@@ -1409,21 +1372,6 @@ dependencies = [
"libc",
]
[[package]]
name = "metal"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15"
dependencies = [
"bitflags 2.10.0",
"block",
"core-graphics-types 0.2.0",
"foreign-types",
"log",
"objc",
"paste",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -1436,9 +1384,9 @@ dependencies = [
[[package]]
name = "moxcms"
version = "0.7.11"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
@@ -1446,9 +1394,9 @@ dependencies = [
[[package]]
name = "naga"
version = "28.0.0"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135"
checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130"
dependencies = [
"arrayvec",
"bit-set",
@@ -1457,11 +1405,11 @@ dependencies = [
"cfg_aliases",
"codespan-reporting",
"half",
"hashbrown 0.16.1",
"hexf-parse",
"hashbrown 0.17.1",
"indexmap",
"libm",
"log",
"naga-types",
"num-traits",
"once_cell",
"rustc-hash",
@@ -1470,6 +1418,18 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "naga-types"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49"
dependencies = [
"hashbrown 0.17.1",
"indexmap",
"rustc-hash",
"thiserror 2.0.17",
]
[[package]]
name = "ndk"
version = "0.9.0"
@@ -1594,15 +1554,6 @@ dependencies = [
"syn 2.0.113",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -1635,13 +1586,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"libc",
"objc2 0.5.2",
"objc2-core-data",
"objc2-core-image",
"objc2-foundation 0.2.2",
"objc2-quartz-core",
"objc2-quartz-core 0.2.2",
]
[[package]]
@@ -1663,7 +1614,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation 0.2.2",
@@ -1675,7 +1626,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -1687,7 +1638,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -1722,10 +1673,10 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal",
"objc2-metal 0.2.2",
]
[[package]]
@@ -1734,7 +1685,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-contacts",
"objc2-foundation 0.2.2",
@@ -1763,7 +1714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"dispatch",
"libc",
"objc2 0.5.2",
@@ -1797,7 +1748,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
@@ -1810,11 +1761,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-metal"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
"objc2 0.6.4",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-quartz-core"
version = "0.2.2"
@@ -1822,10 +1785,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal",
"objc2-metal 0.2.2",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
dependencies = [
"bitflags 2.10.0",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
"objc2-metal 0.3.2",
]
[[package]]
@@ -1845,7 +1822,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-cloud-kit",
"objc2-core-data",
@@ -1853,7 +1830,7 @@ dependencies = [
"objc2-core-location",
"objc2-foundation 0.2.2",
"objc2-link-presentation",
"objc2-quartz-core",
"objc2-quartz-core 0.2.2",
"objc2-symbols",
"objc2-uniform-type-identifiers",
"objc2-user-notifications",
@@ -1865,7 +1842,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -1877,7 +1854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation 0.2.2",
@@ -1971,7 +1948,7 @@ dependencies = [
"linebender_resource_handle",
"parlance",
"parley_data",
"skrifa 0.44.0",
"skrifa",
]
[[package]]
@@ -2275,9 +2252,9 @@ dependencies = [
[[package]]
name = "ravif"
version = "0.12.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285"
checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
dependencies = [
"avif-serialize",
"imgref",
@@ -2294,6 +2271,18 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "raw-window-metal"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
dependencies = [
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"objc2-quartz-core 0.3.2",
]
[[package]]
name = "rayon"
version = "1.11.0"
@@ -2314,16 +2303,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "read-fonts"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eaa2941a4c05443ee3a7b26ab076a553c343ad5995230cc2b1d3e993bdc6345"
dependencies = [
"bytemuck",
"font-types 0.10.1",
]
[[package]]
name = "read-fonts"
version = "0.41.0"
@@ -2331,7 +2310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709"
dependencies = [
"bytemuck",
"font-types 0.12.4",
"font-types",
"once_cell",
]
@@ -2506,16 +2485,6 @@ dependencies = [
"quote",
]
[[package]]
name = "skrifa"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9eb0b904a04d09bd68c65d946617b8ff733009999050f3b851c32fb3cfb60e"
dependencies = [
"bytemuck",
"read-fonts 0.36.0",
]
[[package]]
name = "skrifa"
version = "0.44.0"
@@ -2523,7 +2492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
dependencies = [
"bytemuck",
"read-fonts 0.41.0",
"read-fonts",
]
[[package]]
@@ -2583,9 +2552,9 @@ dependencies = [
[[package]]
name = "spirv"
version = "0.3.0+sdk-1.3.268.0"
version = "0.4.0+sdk-1.4.341.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f"
dependencies = [
"bitflags 2.10.0",
]
@@ -2614,7 +2583,7 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e"
dependencies = [
"skrifa 0.39.0",
"skrifa",
"yazi",
"zeno",
]
@@ -2703,16 +2672,16 @@ dependencies = [
[[package]]
name = "tiff"
version = "0.10.3"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
dependencies = [
"fax",
"flate2",
"half",
"quick-error",
"weezl",
"zune-jpeg 0.4.21",
"zune-jpeg",
]
[[package]]
@@ -2885,9 +2854,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
dependencies = [
"cfg-if",
"once_cell",
@@ -2898,22 +2867,19 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.56"
version = "0.4.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2921,22 +2887,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.113",
"syn 3.0.5",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
dependencies = [
"unicode-ident",
]
@@ -3052,9 +3018,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.83"
version = "0.3.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -3078,9 +3044,9 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "wgpu"
version = "28.0.0"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f"
checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e"
dependencies = [
"arrayvec",
"bitflags 2.10.0",
@@ -3088,7 +3054,7 @@ dependencies = [
"cfg-if",
"cfg_aliases",
"document-features",
"hashbrown 0.16.1",
"hashbrown 0.17.1",
"js-sys",
"log",
"naga",
@@ -3108,9 +3074,9 @@ dependencies = [
[[package]]
name = "wgpu-core"
version = "28.0.0"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb4c8b5db5f00e56f1f08869d870a0dff7c8bc7ebc01091fec140b0cf0211a9"
checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14"
dependencies = [
"arrayvec",
"bit-set",
@@ -3119,10 +3085,11 @@ dependencies = [
"bytemuck",
"cfg_aliases",
"document-features",
"hashbrown 0.16.1",
"hashbrown 0.17.1",
"indexmap",
"log",
"naga",
"naga-types",
"once_cell",
"parking_lot",
"portable-atomic",
@@ -3135,66 +3102,70 @@ dependencies = [
"wgpu-core-deps-emscripten",
"wgpu-core-deps-windows-linux-android",
"wgpu-hal",
"wgpu-naga-bridge",
"wgpu-types",
]
[[package]]
name = "wgpu-core-deps-apple"
version = "28.0.0"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da"
checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-core-deps-emscripten"
version = "28.0.0"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79"
checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-core-deps-windows-linux-android"
version = "28.0.0"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae"
checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-hal"
version = "28.0.0"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "293080d77fdd14d6b08a67c5487dfddbf874534bb7921526db56a7b75d7e3bef"
checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58"
dependencies = [
"android_system_properties",
"arrayvec",
"ash",
"bit-set",
"bitflags 2.10.0",
"block",
"block2 0.6.2",
"bytemuck",
"cfg-if",
"cfg_aliases",
"core-graphics-types 0.2.0",
"glow",
"glutin_wgl_sys",
"gpu-allocator",
"gpu-descriptor",
"hashbrown 0.16.1",
"hashbrown 0.17.1",
"js-sys",
"khronos-egl",
"libc",
"libloading",
"log",
"metal",
"naga",
"naga-types",
"ndk-sys",
"objc",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
"objc2-metal 0.3.2",
"objc2-quartz-core 0.3.2",
"once_cell",
"ordered-float",
"parking_lot",
@@ -3203,26 +3174,44 @@ dependencies = [
"profiling",
"range-alloc",
"raw-window-handle",
"raw-window-metal",
"renderdoc-sys",
"smallvec",
"static_assertions",
"thiserror 2.0.17",
"wasm-bindgen",
"wayland-sys",
"web-sys",
"wgpu-naga-bridge",
"wgpu-types",
"windows",
"windows-core",
"windows-result",
]
[[package]]
name = "wgpu-naga-bridge"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081"
dependencies = [
"naga",
"wgpu-types",
]
[[package]]
name = "wgpu-types"
version = "28.0.0"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c"
checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286"
dependencies = [
"bitflags 2.10.0",
"bytemuck",
"js-sys",
"log",
"naga-types",
"raw-window-handle",
"static_assertions",
"web-sys",
]
@@ -3586,12 +3575,12 @@ dependencies = [
"android-activity",
"atomic-waker",
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"bytemuck",
"calloop",
"cfg_aliases",
"concurrent-queue",
"core-foundation 0.9.4",
"core-foundation",
"core-graphics",
"cursor-icon",
"dpi",
@@ -3858,12 +3847,6 @@ dependencies = [
"syn 3.0.5",
]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-core"
version = "0.5.0"
@@ -3879,20 +3862,11 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "zune-jpeg"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
dependencies = [
"zune-core 0.4.12",
]
[[package]]
name = "zune-jpeg"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5"
dependencies = [
"zune-core 0.5.0",
"zune-core",
]
+2 -2
View File
@@ -29,9 +29,9 @@ edition = "2024"
[workspace.dependencies]
pollster = "0.4.0"
winit = "0.30.12"
wgpu = "28.0.0"
wgpu = "30.0.1"
bytemuck = "1.23.1"
image = "0.25.6"
image = "0.25.10"
parley = "0.11.1"
swash = "0.2.10"
fxhash = "0.2.1"
+3 -3
View File
@@ -1,7 +1,7 @@
use std::ops::{Index, IndexMut};
use crate::{
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
util::to_mut,
};
@@ -39,7 +39,7 @@ struct Child {
tail: usize,
}
pub type PrimitiveLayers = Layers<Primitives>;
pub type DrawLayers = Layers<LayerDraws>;
impl<T: Default> Layers<T> {
pub fn new() -> Layers<T> {
@@ -119,7 +119,7 @@ impl<T: Default> Layers<T> {
}
}
impl PrimitiveLayers {
impl DrawLayers {
pub fn write<P: Primitive>(
&mut self,
layer: LayerId,
+7 -16
View File
@@ -1,6 +1,5 @@
use crate::{
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor,
util::Vec2,
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
};
use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
@@ -159,7 +158,7 @@ impl TextBuffer {
}
impl TextData {
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
pub fn place(&mut self, buffer: &TextBuffer) -> Vec<PlacedGlyph> {
let mut placed = Vec::new();
for line in buffer.layout.lines() {
for item in line.items() {
@@ -185,17 +184,14 @@ impl TextData {
subpixel,
coords: coords_hash,
};
let Some(entry) = self.glyph_entry(
GlyphRaster {
let Some(entry) = self.glyph_entry(GlyphRaster {
key,
font: font_ref,
font_size,
coords,
subpixel,
glyph_id: glyph.id,
},
textures,
) else {
}) else {
continue;
};
placed.push(PlacedGlyph {
@@ -211,11 +207,7 @@ impl TextData {
placed
}
fn glyph_entry(
&mut self,
glyph: GlyphRaster<'_>,
textures: &mut Textures,
) -> Option<GlyphEntry> {
fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option<GlyphEntry> {
if let Some(entry) = self.atlas.get(&glyph.key) {
return entry;
}
@@ -237,7 +229,7 @@ impl TextData {
.render(&mut scaler, glyph.glyph_id as u16);
if let Some(image) = image {
self.atlas.insert(glyph.key, &image, textures)
self.atlas.insert(glyph.key, &image)
} else {
self.atlas.insert_empty(glyph.key);
None
@@ -278,10 +270,9 @@ impl TextData {
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
textures: &mut Textures,
) -> RenderedText {
buffer.shape(self, attrs, width);
let glyphs = self.place(buffer, textures);
let glyphs = self.place(buffer);
RenderedText {
glyphs,
size: buffer.size(),
+18 -19
View File
@@ -1,7 +1,4 @@
use crate::{
render::TexturePrimitive,
util::{RefCounter, Vec2},
};
use crate::util::{RefCounter, Vec2};
use image::{DynamicImage, GenericImageView};
use std::{
ops::Index,
@@ -10,7 +7,7 @@ use std::{
#[derive(Debug, Clone)]
pub struct TextureHandle {
inner: TexturePrimitive,
slot: u32,
size: Vec2,
counter: RefCounter,
send: Sender<u32>,
@@ -31,6 +28,8 @@ pub enum TextureUpdate<'a> {
Set(u32, &'a DynamicImage),
Patch(u32, PatchRect, &'a DynamicImage),
Free(u32),
/// Added and freed before the renderer drained either update. It still has
/// to push a slot to stay lined up with `images`; `Free` then empties it.
PushFree,
SetFree,
}
@@ -64,14 +63,8 @@ impl Textures {
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
let image = image.into();
let size = image.dimensions().into();
let view_idx = self.push(image);
// 0 == default in renderer; TODO: actually create samplers here
let sampler_idx = 0;
TextureHandle {
inner: TexturePrimitive {
view_idx,
sampler_idx,
},
slot: self.push(image),
size,
counter: RefCounter::new(),
send: self.send.clone(),
@@ -92,15 +85,20 @@ impl Textures {
}
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
self.images[handle.inner.view_idx as usize]
self.images[handle.slot as usize]
.as_mut()
.expect("texture was freed while still held")
}
/// Queue an upload of just `rect`, after writing it with `image_mut`.
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
self.updates
.push(Update::Patch(handle.inner.view_idx, 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) {
@@ -131,8 +129,9 @@ impl Textures {
}
impl TextureHandle {
pub fn primitive(&self) -> TexturePrimitive {
self.inner
/// Index into `Textures`, and into the renderer's parallel slots.
pub fn slot(&self) -> u32 {
self.slot
}
pub fn size(&self) -> Vec2 {
self.size
@@ -142,7 +141,7 @@ impl TextureHandle {
impl Drop for TextureHandle {
fn drop(&mut self) {
if self.counter.drop() {
let _ = self.send.send(self.inner.view_idx);
let _ = self.send.send(self.slot);
}
}
}
@@ -151,7 +150,7 @@ impl Index<&TextureHandle> for Textures {
type Output = DynamicImage;
fn index(&self, index: &TextureHandle) -> &Self::Output {
self.images[index.inner.view_idx as usize].as_ref().unwrap()
self.images[index.slot as usize].as_ref().unwrap()
}
}
+49 -36
View File
@@ -1,11 +1,12 @@
use crate::{
PatchRect, TextureHandle, Textures,
PatchRect,
util::{HashMap, Vec2},
};
use image::RgbaImage;
use swash::scale::image::{Content, Image};
const PAGE: u32 = 1024;
/// Side of one page, and so of every layer of `render::page`'s array texture.
pub(crate) const PAGE: u32 = 1024;
/// Transparent margin kept around every glyph, so that sampling one cannot
/// pick up its neighbour along a shared edge.
@@ -35,8 +36,8 @@ pub struct GlyphEntry {
pub width: u32,
pub height: u32,
pub is_colored: bool,
pub view_idx: u32,
pub sampler_idx: u32,
/// Which atlas array layer this glyph is on.
pub layer: u32,
}
impl GlyphEntry {
@@ -48,18 +49,26 @@ impl GlyphEntry {
}
struct Page {
handle: TextureHandle,
image: RgbaImage,
x: u32,
y: u32,
shelf_height: u32,
}
/// A rectangle of one page the renderer has not uploaded yet.
#[derive(Clone, Copy)]
pub struct PageUpload {
pub layer: u32,
pub rect: PatchRect,
}
#[derive(Default)]
pub struct GlyphAtlas {
pages: Vec<Page>,
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
/// too, so it is not re-rasterised on every layout.
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
uploads: Vec<PageUpload>,
}
impl GlyphAtlas {
@@ -67,12 +76,7 @@ impl GlyphAtlas {
self.entries.get(key).copied()
}
pub fn insert(
&mut self,
key: GlyphKey,
image: &Image,
textures: &mut Textures,
) -> Option<GlyphEntry> {
pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option<GlyphEntry> {
let w = image.placement.width;
let h = image.placement.height;
if w == 0 || h == 0 {
@@ -94,23 +98,11 @@ impl GlyphAtlas {
return None;
}
let (page_idx, x, y) = self.allocate(w, h, textures);
let page = &self.pages[page_idx];
let upload = self.allocate(w, h);
let PatchRect { x, y, .. } = upload.rect;
write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y);
self.uploads.push(upload);
let img = textures.image_mut(&page.handle);
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
write_glyph(rgba, image, x, y);
let handle = page.handle.clone();
let rect = PatchRect {
x,
y,
width: w,
height: h,
};
textures.patch(&handle, rect);
let page = &self.pages[page_idx];
let scale = 1.0 / PAGE as f32;
let entry = GlyphEntry {
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
@@ -120,39 +112,60 @@ impl GlyphAtlas {
width: w,
height: h,
is_colored: matches!(image.content, Content::Color),
view_idx: page.handle.primitive().view_idx,
sampler_idx: page.handle.primitive().sampler_idx,
layer: upload.layer,
};
self.entries.insert(key, Some(entry));
Some(entry)
}
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
/// Reserves room for a `w` by `h` glyph, adding a page if none has it.
fn allocate(&mut self, w: u32, h: u32) -> PageUpload {
let rect = |x, y| PatchRect {
x,
y,
width: w,
height: h,
};
if let Some((i, (x, y))) = self
.pages
.iter_mut()
.enumerate()
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
{
return (i, x, y);
return PageUpload {
layer: i as u32,
rect: rect(x, y),
};
}
let handle = textures.add(RgbaImage::new(PAGE, PAGE));
self.pages.push(Page {
handle,
image: RgbaImage::new(PAGE, PAGE),
x: PAD + w + PAD,
y: PAD,
shelf_height: h + PAD,
});
(self.pages.len() - 1, PAD, PAD)
PageUpload {
layer: self.pages.len() as u32 - 1,
rect: rect(PAD, PAD),
}
}
/// Drains what has been written since the last call, for the renderer to
/// upload. A new page needs nothing more: wgpu leaves the rest of a fresh
/// layer transparent, which is what an atlas wants.
pub fn uploads(&mut self) -> impl Iterator<Item = (PageUpload, &RgbaImage)> {
let pages = &self.pages;
self.uploads
.drain(..)
.map(|upload| (upload, &pages[upload.layer as usize].image))
}
pub fn insert_empty(&mut self, key: GlyphKey) {
self.entries.insert(key, None);
}
pub fn page_count(&self) -> usize {
self.pages.len()
pub fn page_count(&self) -> u32 {
self.pages.len() as u32
}
pub fn glyph_count(&self) -> usize {
+1 -5
View File
@@ -12,20 +12,16 @@ pub struct WindowUniform {
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PrimitiveInstance {
pub region: UiRegion,
pub binding: u32,
pub idx: u32,
pub mask_idx: MaskIdx,
}
impl PrimitiveInstance {
const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![
const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
0 => Float32x2,
1 => Float32x2,
2 => Float32x2,
3 => Float32x2,
4 => Uint32,
5 => Uint32,
6 => Uint32,
];
pub fn desc() -> VertexBufferLayout<'static> {
+200 -191
View File
@@ -1,8 +1,6 @@
use std::num::NonZero;
use crate::{
UiData, UiRenderState,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
render::{data::PrimitiveInstance, util::ArrBuf},
util::{HashMap, Vec2},
};
use data::WindowUniform;
@@ -13,6 +11,7 @@ use wgpu::{
mod atlas;
mod data;
mod page;
mod primitive;
mod texture;
mod util;
@@ -21,42 +20,63 @@ pub use atlas::*;
pub use data::{Mask, MaskIdx};
pub use primitive::*;
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
pub struct UiRenderNode {
uniform_group: BindGroup,
primitive_layout: BindGroupLayout,
rsc_layout: BindGroupLayout,
rsc_group: BindGroup,
shared_layout: BindGroupLayout,
shared_group: BindGroup,
format: TextureFormat,
pipeline: RenderPipeline,
/// One per registered primitive, in id order.
primitives: Vec<PrimitivePipeline>,
layers: HashMap<usize, RenderLayer>,
active: Vec<usize>,
window_buffer: Buffer,
textures: GpuTextures,
masks: ArrBuf<Mask>,
}
struct RenderLayer {
/// One per registered primitive, `None` where this layer draws none.
primitives: Vec<Option<ListBuffers>>,
}
/// What draws one registered primitive.
struct PrimitivePipeline {
data_layout: BindGroupLayout,
pipeline: RenderPipeline,
render: Box<dyn PrimitiveRender>,
}
/// One list's vertex buffer and the data its shader reads.
struct ListBuffers {
instance: ArrBuf<PrimitiveInstance>,
primitives: PrimitiveBuffers,
primitive_group: BindGroup,
data: ArrBuf<u8>,
group: Option<BindGroup>,
/// What the primitive asked to keep per instance, if anything.
bindings: Vec<u32>,
}
impl UiRenderNode {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.uniform_group, &[]);
pass.set_bind_group(2, &self.rsc_group, &[]);
pass.set_bind_group(0, &self.shared_group, &[]);
for i in &self.active {
let layer = &self.layers[i];
if layer.instance.len() == 0 {
continue;
for (id, list) in layer.primitives.iter().enumerate() {
let Some(list) = list else { continue };
let Some(group) = &list.group else { continue };
let primitive = &self.primitives[id];
pass.set_pipeline(&primitive.pipeline);
pass.set_bind_group(1, group, &[]);
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
primitive.render.draw(
pass,
ListDraw {
instances: list.instance.len() as u32,
bindings: &list.bindings,
},
);
}
pass.set_bind_group(1, &layer.primitive_group, &[]);
pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
pass.draw(0..4, 0..layer.instance.len() as u32);
}
}
@@ -67,55 +87,56 @@ impl UiRenderNode {
ui: &mut UiData,
ui_render: &mut UiRenderState,
) {
// Before the layers: each list is given its pipeline's data layout.
self.build_pipelines(device, queue, &ui.primitives);
self.active.clear();
for (i, primitives) in ui_render.layers.iter_mut() {
for (i, draws) in ui_render.layers.iter_mut() {
self.active.push(i);
for change in primitives.apply_free() {
for change in draws.apply_free() {
if let Some(inst) = ui_render.active.get_mut(&change.id) {
for h in &mut inst.primitives {
if h.layer == i && h.inst_idx == change.old {
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
h.inst_idx = change.new;
break;
}
}
}
}
let rlayer = self.layers.entry(i).or_insert_with(|| {
let primitives = PrimitiveBuffers::new(device);
let primitive_group =
Self::primitive_group(device, &self.primitive_layout, primitives.buffers());
RenderLayer {
instance: ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"instance",
),
primitives,
primitive_group,
let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new);
if draws.updated {
let lists = draws.primitives();
// The zip would otherwise skip a list with no pipeline.
assert!(lists.len() <= self.primitives.len());
rlayer.primitives.resize_with(lists.len(), || None);
for ((buffers, list), primitive) in rlayer
.primitives
.iter_mut()
.zip(lists)
.zip(&self.primitives)
{
let Some(list) = list else {
continue;
};
buffers
.get_or_insert_with(|| ListBuffers::new(device))
.update(device, queue, primitive, list);
}
});
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(),
);
primitives.updated = false;
draws.updated = false;
}
}
let mut changed = false;
changed |= self.textures.update(&mut ui.textures);
for primitive in &mut self.primitives {
primitive.render.update(ui);
}
if ui.masks.changed {
ui.masks.changed = false;
self.masks.update(device, queue, &ui.masks[..]);
changed = true;
if self.masks.update(device, queue, &ui.masks[..]) {
self.shared_group = Self::shared_group(
device,
&self.shared_layout,
&self.window_buffer,
&self.masks,
);
}
if changed {
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks);
}
}
@@ -128,17 +149,7 @@ impl UiRenderNode {
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
}
pub fn new(
device: &Device,
queue: &Queue,
config: &SurfaceConfiguration,
limits: UiLimits,
) -> Self {
let shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("UI Shape Shader"),
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
});
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
let window_uniform = WindowUniform {
width: config.width as f32,
height: config.height as f32,
@@ -149,67 +160,73 @@ impl UiRenderNode {
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
});
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: &core::array::from_fn::<_, { PrimitiveBuffers::LEN }, _>(|i| {
BindGroupLayoutEntry {
binding: i as u32,
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 shared_layout = Self::shared_layout(device);
let masks = ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks",
);
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
let rsc_layout = Self::rsc_layout(device, &limits);
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
Self {
shared_layout,
shared_group,
format: config.format,
primitives: Vec::new(),
window_buffer,
layers: HashMap::default(),
active: Vec::new(),
masks,
}
}
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
/// Compiles a pipeline for every primitive registered since the last call.
/// Sources only ever arrive at the end, so an id keeps its pipeline.
fn build_pipelines(&mut self, device: &Device, queue: &Queue, registry: &PrimitiveRegistry) {
for source in &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,
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("UI Shape Pipeline"),
layout: Some(&pipeline_layout),
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
self.primitives.push(PrimitivePipeline {
data_layout,
pipeline,
render,
});
}
}
fn pipeline(
device: &Device,
layout: &PipelineLayout,
format: TextureFormat,
wgsl: &str,
label: &str,
) -> RenderPipeline {
let module = device.create_shader_module(ShaderModuleDescriptor {
label: Some(label),
source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()),
});
device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some(label),
layout: Some(layout),
vertex: VertexState {
module: &shader,
module: &module,
entry_point: Some("vs_main"),
buffers: &[PrimitiveInstance::desc()],
buffers: &[Some(PrimitiveInstance::desc())],
compilation_options: Default::default(),
},
fragment: Some(FragmentState {
module: &shader,
module: &module,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format: config.format,
format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
@@ -232,90 +249,42 @@ impl UiRenderNode {
},
multiview_mask: None,
cache: None,
});
Self {
uniform_group,
primitive_layout,
rsc_layout,
rsc_group,
pipeline,
window_buffer,
layers: HashMap::default(),
active: Vec::new(),
textures: tex_manager,
masks,
}
}
fn bind_group_0(
device: &Device,
layout: &BindGroupLayout,
window_buffer: &Buffer,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[BindGroupEntry {
binding: 0,
resource: window_buffer.as_entire_binding(),
}],
label: Some("ui window"),
})
}
fn primitive_group(
device: &Device,
layout: &BindGroupLayout,
buffers: [(u32, &Buffer); PrimitiveBuffers::LEN],
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &buffers.map(|(binding, buf)| BindGroupEntry {
binding,
resource: buf.as_entire_binding(),
}),
label: Some("ui primitives"),
})
}
fn rsc_layout(device: &Device, limits: &UiLimits) -> BindGroupLayout {
/// What every draw in the ui is given: the window and the masks.
fn shared_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2,
multisampled: false,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: BufferSize::new(size_of::<WindowUniform>() as u64),
},
count: Some(NonZero::new(limits.max_textures).unwrap()),
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: Some(NonZero::new(limits.max_samplers).unwrap()),
},
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
min_binding_size: BufferSize::new(size_of::<Mask>() as u64),
},
count: None,
},
],
label: Some("ui rsc"),
label: Some("ui shared"),
})
}
fn rsc_group(
fn shared_group(
device: &Device,
layout: &BindGroupLayout,
tex_manager: &GpuTextures,
window: &Buffer,
masks: &ArrBuf<Mask>,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
@@ -323,45 +292,85 @@ impl UiRenderNode {
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureViewArray(&tex_manager.views()),
resource: window.as_entire_binding(),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::SamplerArray(&tex_manager.samplers()),
},
BindGroupEntry {
binding: 2,
resource: masks.buffer.as_entire_binding(),
},
],
label: Some("ui rsc"),
label: Some("ui shared"),
})
}
pub fn view_count(&self) -> usize {
self.textures.view_count()
/// Layout for a list of one primitive's data. Every size in the ui is
/// stated, so "is the buffer big enough for one entry?" is answered when
/// the bind group is made; a `None` size is wgpu's to check on every draw.
fn data_layout(device: &Device, stride: u64) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: BufferSize::new(stride),
},
count: None,
}],
label: Some("ui primitive data"),
})
}
}
pub struct UiLimits {
max_textures: u32,
max_samplers: u32,
}
impl Default for UiLimits {
fn default() -> Self {
impl RenderLayer {
fn new() -> Self {
Self {
max_textures: 100000,
max_samplers: 1000,
primitives: Vec::new(),
}
}
}
impl UiLimits {
pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 {
self.max_textures + self.max_samplers
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(),
}
pub fn max_binding_array_sampler_elements_per_shader_stage(&self) -> u32 {
self.max_samplers
}
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
@@ -0,0 +1,156 @@
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: &[],
})
}
+304 -204
View File
@@ -1,115 +1,247 @@
use std::ops::{Deref, DerefMut};
use std::{any::TypeId, marker::PhantomData};
use crate::{
Color, UiRegion, WidgetId,
Color, TextureHandle, UiData, UiRegion, WidgetId,
render::{
ArrBuf,
data::{MaskIdx, PrimitiveInstance},
page::GlyphRender,
texture::ImageRender,
},
util::Vec2,
util::{HashMap, Vec2},
};
use bytemuck::Pod;
use wgpu::*;
use wgpu::{BindGroupLayout, Device, Queue, RenderPass};
pub struct Primitives {
/// One instance of a primitive, laid out as the struct its shader reads.
///
/// The type carries its own shader, so drawing one is all the wiring it needs:
/// its list, free list, buffers and pipeline follow from being registered.
pub trait Primitive: Pod + 'static {
/// Compiled after `prelude.wgsl`, which states what it declares and what
/// it is given.
const WGSL: &'static str;
/// Made once, the first time the renderer sees this primitive. It owns
/// whatever the shader samples and records the primitive's own draws; the
/// default owns nothing and draws every instance in one call.
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender>
where
Self: Sized,
{
let _ = (device, queue);
Box::new(Instanced)
}
}
/// The renderer's half of a primitive: what it samples, what it uploads, and
/// what draws it records.
///
/// Everything a draw shares -- the pipeline, the window and masks, the list's
/// own data and instance buffer -- is set before this is called. What is left
/// is what only this primitive knows: its group 2, and how many draws its
/// instances are.
pub trait PrimitiveRender {
/// The layout its shader reads at group 2. `None` for a primitive whose
/// shader samples nothing, whose pipeline then has no group 2 at all.
fn layout(&self) -> Option<&BindGroupLayout> {
None
}
/// Uploads whatever this primitive owns, once a frame, before any draw.
fn update(&mut self, ui: &mut UiData) {
let _ = ui;
}
/// Keeps what the primitive needs per instance at draw time, read from
/// the list's own data. A primitive that binds nothing per instance --
/// most of them -- leaves this empty and draws in one call.
fn instance_bindings(&self, list: &InstanceList, out: &mut Vec<u32>) {
let _ = (list, out);
}
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>);
}
/// What a `PrimitiveRender` draws: this list's instances, and whatever
/// `instance_bindings` kept for them.
pub struct ListDraw<'a> {
pub instances: u32,
pub bindings: &'a [u32],
}
/// The default: nothing sampled, every instance in one call.
pub struct Instanced;
impl PrimitiveRender for Instanced {
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
pass.draw(0..4, 0..list.instances);
}
}
/// Which registered primitive an instance is.
pub struct PrimitiveKind<P> {
id: u32,
_p: PhantomData<fn(P)>,
}
impl<P> PrimitiveKind<P> {
fn new(id: u32) -> Self {
Self {
id,
_p: PhantomData,
}
}
}
impl<P> Clone for PrimitiveKind<P> {
fn clone(&self) -> Self {
*self
}
}
impl<P> Copy for PrimitiveKind<P> {}
/// Every primitive a ui can draw, in the order they were first drawn.
#[derive(Default)]
pub struct PrimitiveRegistry {
kinds: Vec<PrimitiveSource>,
ids: HashMap<TypeId, u32>,
}
pub struct PrimitiveSource {
pub wgsl: &'static str,
pub label: &'static str,
/// Size of one instance's entry, stated as the data binding's minimum.
pub stride: u64,
pub render: fn(&Device, &Queue) -> Box<dyn PrimitiveRender>,
}
impl PrimitiveRegistry {
/// Registers `P` if this is the first time it has been drawn.
pub fn kind<P: Primitive>(&mut self) -> PrimitiveKind<P> {
let Self { kinds, ids } = self;
let id = *ids.entry(TypeId::of::<P>()).or_insert_with(|| {
kinds.push(PrimitiveSource {
wgsl: P::WGSL,
label: std::any::type_name::<P>(),
stride: size_of::<P>() as u64,
render: P::render,
});
kinds.len() as u32 - 1
});
PrimitiveKind::new(id)
}
pub fn sources(&self) -> &[PrimitiveSource] {
&self.kinds
}
}
/// One registered primitive's instances in one layer. Everything per-instance
/// rides here, so it stays in step through a `swap_remove`.
pub struct InstanceList {
instances: Vec<PrimitiveInstance>,
/// The widget each instance belongs to, for renumbering its handles.
assoc: Vec<WidgetId>,
data: PrimitiveData,
free: Vec<usize>,
/// `stride` bytes of the primitive's own data per instance.
data: Vec<u8>,
/// From the type the list was made for, so a write is never checked.
stride: usize,
}
impl InstanceList {
fn new<P: Primitive>() -> Self {
Self {
instances: Vec::new(),
assoc: Vec::new(),
free: Vec::new(),
data: Vec::new(),
stride: size_of::<P>(),
}
}
pub fn instances(&self) -> &[PrimitiveInstance] {
&self.instances
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn stride(&self) -> usize {
self.stride
}
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> usize {
if let Some(i) = self.free.pop() {
self.instances[i] = inst;
self.assoc[i] = id;
self.data[i * self.stride..][..self.stride].copy_from_slice(data);
i
} else {
let i = self.instances.len();
self.instances.push(inst);
self.assoc.push(id);
self.data.extend_from_slice(data);
i
}
}
fn free(&mut self, i: usize) -> MaskIdx {
self.free.push(i);
self.instances[i].mask_idx
}
fn apply_free(&mut self, kind: u32) -> impl Iterator<Item = PrimitiveChange> {
self.free.sort_by(|a, b| b.cmp(a));
let instances = &mut self.instances;
let assoc = &mut self.assoc;
let data = &mut self.data;
let stride = self.stride;
self.free.drain(..).filter_map(move |i| {
instances.swap_remove(i);
assoc.swap_remove(i);
let last = instances.len();
data.copy_within(last * stride..(last + 1) * stride, i * stride);
data.truncate(last * stride);
if i == last {
return None;
}
let id = assoc[i];
Some(PrimitiveChange {
id,
kind,
old: last,
new: i,
})
})
}
}
/// Everything one layer draws, one list per registered primitive.
pub struct LayerDraws {
/// `None` until this layer draws that primitive, because only the write
/// knows the type the list is for.
primitives: Vec<Option<InstanceList>>,
pub updated: bool,
}
impl Default for Primitives {
impl Default for LayerDraws {
fn default() -> Self {
Self {
instances: Default::default(),
assoc: Default::default(),
data: Default::default(),
free: Vec::new(),
primitives: Vec::new(),
updated: true,
}
}
}
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)*);
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 {
impl LayerDraws {
pub fn write<P: Primitive>(
&mut self,
layer: usize,
PrimitiveInst {
kind,
id,
primitive,
region,
@@ -117,65 +249,67 @@ impl Primitives {
}: PrimitiveInst<P>,
) -> PrimitiveHandle {
self.updated = true;
let vec = P::vec(&mut self.data);
let i = vec.add(primitive);
let inst = PrimitiveInstance {
region,
idx: i as u32,
mask_idx,
binding: P::BINDING,
};
let inst_i = if let Some(i) = self.free.pop() {
self.instances[i] = inst;
self.assoc[i] = id;
i
} else {
let i = self.instances.len();
self.instances.push(inst);
self.assoc.push(id);
i
};
PrimitiveHandle::new::<P>(layer, inst_i, i)
// Grown on first use rather than sized from the registry, which a
// layer cannot see.
if self.primitives.len() <= kind.id as usize {
self.primitives.resize_with(kind.id as usize + 1, || None);
}
let inst_idx = self.primitives[kind.id as usize]
.get_or_insert_with(InstanceList::new::<P>)
.push(
id,
PrimitiveInstance { region, mask_idx },
bytemuck::bytes_of(&primitive),
);
PrimitiveHandle {
layer,
kind: kind.id,
inst_idx,
}
}
/// returns (old index, new index)
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
self.free.sort_by(|a, b| b.cmp(a));
self.free.drain(..).filter_map(|i| {
self.instances.swap_remove(i);
self.assoc.swap_remove(i);
if i == self.instances.len() {
return None;
pub fn primitives(&self) -> &[Option<InstanceList>] {
&self.primitives
}
let id = self.assoc[i];
let old = self.instances.len();
Some(PrimitiveChange { id, old, new: i })
})
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
self.primitives
.iter_mut()
.enumerate()
.filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?)))
.flat_map(|(kind, list)| list.apply_free(kind))
}
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self.updated = true;
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
self.list(h).free(h.inst_idx)
}
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
self.updated = true;
&mut self.instances[h.inst_idx].region
&mut self.list(h).instances[h.inst_idx].region
}
/// A handle is only ever made by `write`, which is what created the list.
fn list(&mut self, h: &PrimitiveHandle) -> &mut InstanceList {
self.primitives[h.kind as usize]
.as_mut()
.expect("handle names a primitive this layer never drew")
}
}
pub struct PrimitiveInst<P> {
pub kind: PrimitiveKind<P>,
pub id: WidgetId,
pub primitive: P,
pub region: UiRegion,
pub mask_idx: MaskIdx,
}
pub struct PrimitiveChange {
pub id: WidgetId,
/// Which registered primitive's list moved, since they index separately.
pub kind: u32,
pub old: usize,
pub new: usize,
}
@@ -183,30 +317,12 @@ pub struct PrimitiveChange {
#[derive(Debug)]
pub struct PrimitiveHandle {
pub layer: usize,
pub kind: u32,
pub inst_idx: usize,
pub data_idx: usize,
pub binding: u32,
}
impl PrimitiveHandle {
fn new<P: Primitive>(layer: usize, inst_idx: usize, data_idx: usize) -> Self {
Self {
layer,
inst_idx,
data_idx,
binding: P::BINDING,
}
}
}
primitives!(
rects: RectPrimitive => 0,
textures: TexturePrimitive => 1,
glyphs: GlyphPrimitive => 2,
);
#[repr(C)]
#[derive(Copy, Clone)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RectPrimitive {
pub color: Color<u8>,
pub radius: f32,
@@ -214,6 +330,10 @@ pub struct RectPrimitive {
pub inner_radius: f32,
}
impl Primitive for RectPrimitive {
const WGSL: &'static str = include_str!("shader/rect.wgsl");
}
impl RectPrimitive {
pub fn color(color: Color<u8>) -> Self {
Self {
@@ -225,71 +345,51 @@ impl RectPrimitive {
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct TexturePrimitive {
pub view_idx: u32,
pub sampler_idx: u32,
}
#[repr(C)]
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
#[repr(C, align(8))]
#[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive {
pub uv_min: Vec2,
pub uv_max: Vec2,
pub view_idx: u32,
pub sampler_idx: u32,
/// Which atlas array layer this glyph is on.
pub layer: u32,
pub color: Color<u8>,
pub flags: u32,
}
pub struct PrimitiveVec<T> {
vec: Vec<T>,
free: Vec<usize>,
// Manual rather than derived: the align(8) leaves four bytes of padding, which
// is how WGSL lays the struct out.
unsafe impl bytemuck::Pod for GlyphPrimitive {}
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
impl Primitive for GlyphPrimitive {
const WGSL: &'static str = include_str!("shader/glyph.wgsl");
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
Box::new(GlyphRender::new(device, queue))
}
}
impl<T> PrimitiveVec<T> {
pub fn new() -> Self {
/// 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 {
vec: Vec::new(),
free: Vec::new(),
slot: handle.slot(),
}
}
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
}
}
-204
View File
@@ -1,204 +0,0 @@
const RECT: u32 = 0u;
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(TEXTURE)
var<storage> textures: array<TextureInfo>;
@group(1) @binding(GLYPH)
var<storage> glyphs: array<GlyphInfo>;
struct Rect {
color: u32,
radius: f32,
thickness: f32,
inner_radius: f32,
}
struct TextureInfo {
view_idx: u32,
sampler_idx: u32,
}
struct GlyphInfo {
uv_min: vec2<f32>,
uv_max: vec2<f32>,
view_idx: u32,
sampler_idx: 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>,
}
@group(2) @binding(0)
var views: binding_array<texture_2d<f32>>;
@group(2) @binding(1)
var samplers: binding_array<sampler>;
@group(2) @binding(2)
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, textures[i]);
}
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;
}
// TODO: this seems really inefficient (per frag indexing)?
fn draw_texture(region: Region, info: TextureInfo) -> vec4<f32> {
return textureSample(views[info.view_idx], samplers[info.sampler_idx], 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(views[g.view_idx], samplers[g.sampler_idx], uv);
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
@@ -0,0 +1,33 @@
// 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
@@ -0,0 +1,96 @@
// 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
@@ -0,0 +1,39 @@
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
@@ -0,0 +1,10 @@
// 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));
}
+191 -139
View File
@@ -1,119 +1,119 @@
use image::{DynamicImage, EncodableLayout, GenericImageView};
use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage};
use wgpu::{util::DeviceExt, *};
use crate::{PatchRect, TextureUpdate, Textures};
use crate::{
PatchRect, TextureUpdate, Textures, UiData,
render::{
TexturePrimitive,
primitive::{ListDraw, PrimitiveRender},
},
};
/// Draws standalone images, which it owns. Each is its own texture, so each
/// instance binds its own and is a draw of its own.
pub struct ImageRender {
textures: GpuTextures,
layout: BindGroupLayout,
sampler: Sampler,
}
impl ImageRender {
pub fn new(device: &Device, queue: &Queue) -> Self {
Self {
textures: GpuTextures::new(device, queue),
layout: sampled_layout(device, TextureViewDimension::D2, "ui image"),
sampler: default_sampler(device),
}
}
}
impl PrimitiveRender for ImageRender {
fn layout(&self) -> Option<&BindGroupLayout> {
Some(&self.layout)
}
fn update(&mut self, ui: &mut UiData) {
self.textures
.update(&mut ui.textures, &self.layout, &self.sampler);
}
fn instance_bindings(&self, list: &super::InstanceList, out: &mut Vec<u32>) {
let slots = list
.data()
.chunks_exact(list.stride())
.map(|data| bytemuck::pod_read_unaligned::<TexturePrimitive>(data).slot);
out.extend(slots);
}
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
for (i, &slot) in list.bindings.iter().enumerate() {
let Some(image) = self.textures.group(slot) else {
continue;
};
pass.set_bind_group(2, image, &[]);
pass.draw(0..4, i as u32..i as u32 + 1);
}
}
}
/// The standalone images a ui draws, each its own texture and bind group --
/// unlike the glyph atlas in `super::page`, which is one array they share.
pub struct GpuTextures {
device: Device,
queue: Queue,
/// Parallel to `views`; patches require textures rather than views.
textures: Vec<Option<Texture>>,
views: Vec<TextureView>,
view_count: usize,
samplers: Vec<Sampler>,
null_view: TextureView,
no_views: Vec<TextureView>,
slots: Vec<Option<ImageGpu>>,
}
struct ImageGpu {
/// Kept for `patch`, which needs the texture rather than the view.
texture: Texture,
group: BindGroup,
}
impl GpuTextures {
pub fn update(&mut self, textures: &mut Textures) -> bool {
let mut bindings_changed = false;
pub fn new(device: &Device, queue: &Queue) -> Self {
Self {
device: device.clone(),
queue: queue.clone(),
slots: Vec::new(),
}
}
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
for update in textures.updates() {
bindings_changed |= match update {
match update {
TextureUpdate::Push(image) => {
self.push(image);
true
let image = self.create(image, layout, sampler);
self.slots.push(Some(image));
}
TextureUpdate::Set(i, image) => {
self.set(i, image);
true
let image = self.create(image, layout, sampler);
self.slots[i as usize] = Some(image);
}
TextureUpdate::Patch(i, rect, image) => {
self.patch(i, rect, image);
false
TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
TextureUpdate::PushFree => self.slots.push(None),
TextureUpdate::SetFree => {}
TextureUpdate::Free(i) => self.slots[i as usize] = None,
}
TextureUpdate::SetFree => {
self.view_count += 1;
true
}
TextureUpdate::Free(i) => {
self.free(i);
true
}
TextureUpdate::PushFree => {
self.push_free();
true
}
};
}
bindings_changed
}
fn set(&mut self, i: u32, image: &DynamicImage) {
self.view_count += 1;
let (texture, view) = self.create(image);
self.textures[i as usize] = Some(texture);
self.views[i as usize] = view;
}
fn free(&mut self, i: u32) {
self.view_count -= 1;
self.textures[i as usize] = None;
self.views[i as usize] = self.null_view.clone();
}
fn push(&mut self, image: &DynamicImage) {
self.view_count += 1;
let (texture, view) = self.create(image);
self.textures.push(Some(texture));
self.views.push(view);
}
fn push_free(&mut self) {
self.view_count += 1;
self.textures.push(None);
self.views.push(self.null_view.clone());
}
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
let Some(texture) = &self.textures[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,
mip_level: 0,
origin: Origin3d {
x: rect.x,
y: rect.y,
z: 0,
},
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,
},
);
pub fn group(&self, slot: u32) -> Option<&BindGroup> {
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
}
fn create(&self, image: &DynamicImage) -> (Texture, TextureView) {
let image = image.to_rgba8();
let (width, height) = image.dimensions();
fn create(
&self,
image: &DynamicImage,
layout: &BindGroupLayout,
sampler: &Sampler,
) -> ImageGpu {
let rgba = image.to_rgba8();
let (width, height) = rgba.dimensions();
let texture = self.device.create_texture_with_data(
&self.queue,
&TextureDescriptor {
label: None,
label: Some("image"),
size: Extent3d {
width,
height,
@@ -127,63 +127,115 @@ impl GpuTextures {
view_formats: &[],
},
wgt::TextureDataOrder::MipMajor,
image.as_bytes(),
rgba.as_bytes(),
);
let view = texture.create_view(&TextureViewDescriptor::default());
(texture, view)
let group = sampled_group(&self.device, layout, &view, sampler, "ui image");
ImageGpu { texture, group }
}
pub fn new(device: &Device, queue: &Queue) -> Self {
let null_view = null_texture_view(device);
Self {
device: device.clone(),
queue: queue.clone(),
textures: Vec::new(),
views: Vec::new(),
samplers: vec![default_sampler(device)],
no_views: vec![null_view.clone()],
null_view,
view_count: 0,
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
let Some(Some(slot)) = self.slots.get(i as usize) else {
return;
};
let dst = TexelCopyTextureInfo {
texture: &slot.texture,
mip_level: 0,
origin: Origin3d {
x: rect.x,
y: rect.y,
z: 0,
},
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 views(&self) -> Vec<&TextureView> {
if self.views.is_empty() {
&self.no_views
} else {
&self.views
pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) {
if rect.width == 0 || rect.height == 0 {
return;
}
.iter()
.by_ref()
.collect()
}
pub fn samplers(&self) -> Vec<&Sampler> {
self.samplers.iter().by_ref().collect()
}
pub fn view_count(&self) -> usize {
self.view_count
}
}
pub fn null_texture_view(device: &Device) -> TextureView {
device
.create_texture(&TextureDescriptor {
label: Some("null"),
size: Extent3d {
width: 1,
height: 1,
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,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[],
);
}
/// What a primitive that samples binds: a texture, and the sampler that reads
/// it.
pub fn sampled_group(
device: &Device,
layout: &BindGroupLayout,
view: &TextureView,
sampler: &Sampler,
label: &'static str,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(view),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::Sampler(sampler),
},
],
label: Some(label),
})
}
/// The layout for one of those. The dimension differs -- the atlas is an
/// array of pages and an image is not -- and nothing else does.
pub fn sampled_layout(
device: &Device,
dimension: TextureViewDimension,
label: &'static str,
) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: dimension,
multisampled: false,
},
count: None,
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: None,
},
],
label: Some(label),
})
.create_view(&TextureViewDescriptor::default())
}
pub fn default_sampler(device: &Device) -> Sampler {
+10 -6
View File
@@ -21,17 +21,25 @@ impl<T: Pod> ArrBuf<T> {
_pd: PhantomData,
}
}
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) {
if self.len != data.len() {
/// Returns whether the `Buffer` was recreated, which stales any cached
/// `BindGroup` holding it.
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
let resized = self.len != data.len();
if resized {
self.len = data.len();
self.buffer =
Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label);
}
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
resized
}
pub fn len(&self) -> usize {
self.len
}
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) {
// A binding cannot be empty or under the layout's minimum.
size = size.max(std::mem::size_of::<T>() as u64);
}
device.create_buffer(&BufferDescriptor {
@@ -41,8 +49,4 @@ impl<T: Pod> ArrBuf<T> {
usage,
})
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.len
}
}
+6 -2
View File
@@ -1,4 +1,6 @@
use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena};
use crate::{
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
};
mod active;
mod cache;
@@ -7,13 +9,15 @@ mod render_state;
mod size;
pub use active::*;
pub use painter::Painter;
pub use painter::{Painter, PrimitiveLike};
pub use render_state::*;
pub use size::*;
#[derive(Default)]
pub struct UiData {
pub widgets: Widgets,
/// Every primitive this ui can draw.
pub primitives: PrimitiveRegistry,
pub textures: Textures,
pub text: TextData,
pub masks: TrackedArena<Mask, u32>,
+50 -22
View File
@@ -1,7 +1,10 @@
use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
render::{
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
},
util::Vec2,
};
@@ -21,15 +24,26 @@ pub struct Painter<'a> {
impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region);
}
/// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
let h = self.state.layers.write(
self.layer,
PrimitiveInst {
kind,
id: self.id,
primitive,
region,
mask_idx: self.mask,
},
);
self.push_primitive(h);
}
fn push_primitive(&mut self, h: PrimitiveHandle) {
if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -38,11 +52,13 @@ impl<'a> Painter<'a> {
}
/// Writes a primitive to be rendered
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, self.region)
}
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, region.within(&self.region));
}
@@ -75,21 +91,6 @@ impl<'a> Painter<'a> {
);
}
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
self.textures.push(handle.clone());
self.primitive_at(handle.primitive(), region.within(&self.region));
}
pub fn texture(&mut self, handle: &TextureHandle) {
self.textures.push(handle.clone());
self.primitive(handle.primitive());
}
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
self.textures.push(handle.clone());
self.primitive_at(handle.primitive(), region);
}
pub fn render_text(
&mut self,
buffer: &mut TextBuffer,
@@ -97,10 +98,12 @@ impl<'a> Painter<'a> {
width: Option<f32>,
) -> RenderedText {
let ui = self.rsc.ui_mut();
ui.text.render(buffer, attrs, width, &mut ui.textures)
ui.text.render(buffer, attrs, width)
}
// TODO: merge the text methods into the primitive ones.
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() {
let mut region = origin;
region.x.end = region.x.start;
@@ -108,12 +111,12 @@ impl<'a> Painter<'a> {
let mut region = region.offset(UiVec2::abs(glyph.offset));
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
self.primitive_at(
self.write(
kind,
GlyphPrimitive {
uv_min: glyph.entry.uv_min,
uv_max: glyph.entry.uv_max,
view_idx: glyph.entry.view_idx,
sampler_idx: glyph.entry.sampler_idx,
layer: glyph.entry.layer,
color: text.color,
flags: glyph.entry.flags(),
},
@@ -169,3 +172,28 @@ impl<'a> Painter<'a> {
self.state.size_ctx(self.id, self.region.size(), self.rsc)
}
}
/// What `Painter::primitive` takes: a primitive, or something that yields one
/// and does whatever else drawing it needs.
pub trait PrimitiveLike {
type Primitive: Primitive;
fn into_primitive(self, painter: &mut Painter) -> Self::Primitive;
}
impl<P: Primitive> PrimitiveLike for P {
type Primitive = P;
fn into_primitive(self, _: &mut Painter) -> P {
self
}
}
impl PrimitiveLike for &TextureHandle {
type Primitive = TexturePrimitive;
/// Retains a share of the handle, so the slot the primitive names cannot
/// be freed and reused while it is still drawn.
fn into_primitive(self, painter: &mut Painter) -> TexturePrimitive {
painter.textures.push(self.clone());
self.into()
}
}
+10 -11
View File
@@ -1,13 +1,13 @@
use crate::{
ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx,
StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget,
UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
ui::cache::Cache,
util::{HashMap, HashSet, Vec2, forget_ref},
};
pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>,
pub layers: PrimitiveLayers,
pub layers: DrawLayers,
pub(super) output_size: Vec2,
pub cache: Cache,
@@ -243,14 +243,14 @@ impl UiRenderState {
}
pub fn debug_layers(&self) {
for ((idx, depth), primitives) in self.layers.iter_depth() {
for ((idx, depth), draws) in self.layers.iter_depth() {
let indent = " ".repeat(depth * 2);
let len = primitives.instances().len();
print!("{indent}{idx}: {len} primitives");
if len >= 1 {
print!(" ({})", primitives.instances()[0].binding);
}
println!();
let counts: Vec<String> = draws
.primitives()
.iter()
.map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string())
.collect();
println!("{indent}{idx}: [{}]", counts.join(", "));
}
}
@@ -308,7 +308,6 @@ impl UiRenderState {
source,
cache: &mut self.cache,
text: &mut ui.text,
textures: &mut ui.textures,
widgets: &ui.widgets,
outer,
output_size: self.output_size,
+3 -5
View File
@@ -1,11 +1,10 @@
use crate::{
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
};
pub struct SizeCtx<'a> {
pub text: &'a mut TextData,
pub textures: &'a mut Textures,
pub(super) source: WidgetId,
pub(super) widgets: &'a Widgets,
pub(super) cache: &'a mut Cache,
@@ -33,7 +32,6 @@ impl SizeCtx<'_> {
.get_dyn_dynamic(id)
.desired_len::<A>(&mut SizeCtx {
text: self.text,
textures: self.textures,
source: self.source,
widgets: self.widgets,
cache: self.cache,
@@ -82,7 +80,7 @@ impl SizeCtx<'_> {
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
self.text.render(buffer, attrs, width, self.textures)
self.text.render(buffer, attrs, width)
}
pub fn label(&self, id: WidgetId) -> &String {
+2 -2
View File
@@ -212,10 +212,10 @@ impl DefaultAppState for Client {
render: &mut UiRenderState,
) {
let new = format!(
"widgets: {}\nactive: {}\nviews: {}",
"widgets: {}\nactive: {}\ntextures: {}",
rsc.widgets().len(),
render.active_widgets(),
self.ui_state.renderer.ui.view_count(),
rsc.ui().textures.count(),
);
if new != *rsc.widgets()[self.info].content {
*rsc.widgets_mut()[self.info].content = new;
+1 -5
View File
@@ -1,10 +1,6 @@
use crate::prelude::*;
use arboard::Clipboard;
use std::{
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use std::{marker::PhantomData, sync::Arc, time::Instant};
use winit::{
event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
+22 -15
View File
@@ -1,4 +1,4 @@
use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState};
use iris_core::{UiData, UiRenderNode, UiRenderState};
use pollster::FutureExt;
use std::sync::Arc;
use wgpu::*;
@@ -22,7 +22,20 @@ impl UiRenderer {
}
pub fn draw(&mut self) {
let output = self.surface.get_current_texture().unwrap();
let output = match self.surface.get_current_texture() {
CurrentSurfaceTexture::Success(texture) => texture,
CurrentSurfaceTexture::Suboptimal(texture) => {
self.surface.configure(&self.device, &self.config);
texture
}
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
self.surface.configure(&self.device, &self.config);
return;
}
CurrentSurfaceTexture::Timeout
| CurrentSurfaceTexture::Occluded
| CurrentSurfaceTexture::Validation => return,
};
let view = output
.texture
.create_view(&TextureViewDescriptor::default());
@@ -46,7 +59,7 @@ impl UiRenderer {
self.queue.submit(std::iter::once(encoder.finish()));
self.window.pre_present_notify();
output.present();
self.queue.present(output);
}
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
@@ -65,9 +78,10 @@ impl UiRenderer {
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
let instance = Instance::new(&InstanceDescriptor {
let instance = Instance::new(InstanceDescriptor {
backends: Backends::PRIMARY,
..Default::default()
display: Some(Box::new(window.clone())),
..InstanceDescriptor::new_without_display_handle()
});
let surface = instance
@@ -79,22 +93,14 @@ impl UiRenderer {
power_preference: PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
apply_limit_buckets: false,
})
.block_on()
.expect("Could not get adapter!");
let ui_limits = UiLimits::default();
let (device, queue) = adapter
.request_device(&DeviceDescriptor {
required_features: Features::TEXTURE_BINDING_ARRAY
| Features::PARTIALLY_BOUND_BINDING_ARRAY
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
required_limits: Limits {
max_binding_array_elements_per_shader_stage: ui_limits
.max_binding_array_elements_per_shader_stage(),
max_binding_array_sampler_elements_per_shader_stage: ui_limits
.max_binding_array_sampler_elements_per_shader_stage(),
max_buffer_size: 1 << 30,
..Default::default()
},
@@ -114,6 +120,7 @@ impl UiRenderer {
let config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
color_space: SurfaceColorSpace::Auto,
width: size.width,
height: size.height,
present_mode: PresentMode::AutoVsync,
@@ -126,7 +133,7 @@ impl UiRenderer {
let encoder = Self::create_encoder(&device);
let ui = UiRenderNode::new(&device, &queue, &config, ui_limits);
let ui = UiRenderNode::new(&device, &config);
Self {
surface,
+1 -1
View File
@@ -7,7 +7,7 @@ pub struct Image {
impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) {
painter.texture(&self.handle);
painter.primitive(&self.handle);
}
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
+2 -2
View File
@@ -4,7 +4,7 @@ mod max_size;
mod offset;
mod pad;
mod scroll;
mod sized;
mod set_size;
mod span;
mod stack;
@@ -14,6 +14,6 @@ pub use max_size::*;
pub use offset::*;
pub use pad::*;
pub use scroll::*;
pub use sized::*;
pub use set_size::*;
pub use span::*;
pub use stack::*;
@@ -1,12 +1,12 @@
use crate::prelude::*;
pub struct Sized {
pub struct SetSize {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Sized {
impl SetSize {
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest());
@@ -17,7 +17,7 @@ impl Sized {
}
}
impl Widget for Sized {
impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner);
}
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*;
use std::marker::{Sized, Unsize};
use std::marker::Unsize;
pub struct WidgetPtr {
pub inner: Option<StrongWidget>,
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*;
use std::marker::{PhantomData, Sized};
use std::marker::PhantomData;
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
pub content: String,
+6 -6
View File
@@ -31,9 +31,9 @@ widget_trait! {
}
}
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> {
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> {
let size = size.into();
move |state| Sized {
move |state| SetSize {
inner: self.add_strong(state),
x: Some(size.x),
y: Some(size.y),
@@ -58,18 +58,18 @@ widget_trait! {
}
}
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into();
move |state| Sized {
move |state| SetSize {
inner: self.add_strong(state),
x: Some(len),
y: None,
}
}
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into();
move |state| Sized {
move |state| SetSize {
inner: self.add_strong(state),
x: None,
y: Some(len),
+205
View File
@@ -0,0 +1,205 @@
//! 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
);
}
}