Compare commits

..
Author SHA1 Message Date
iris 7cefc72f97 Compare regions, not four loose numbers
`PixelRegion` derives `PartialEq`, `Clone` and `Copy`, so `assert_corners!`
compares one against another instead of flattening both to a tuple whose
order there was nothing to check. A failure now prints two regions.
2026-09-13 21:42:21 -04:00
iris 0f9f379cec Say what these do without naming what they lean on
A doc comment that names something outside the method goes stale when
that thing changes, and nobody editing it looks here.
2026-09-13 21:37:44 -04:00
iris 7eb2b85425 One proxy, and a macro instead of a shared test module
`Proxy` is the task queue as well as the way an application sends its own
events, so `ProxyQueue` is gone. `schedule_redraw` becomes
`request_redraw_if_needed`, which says what the comment beside it was
saying.

`assert_corners!` replaces the region helper, so `tests/common` goes with
it, and the scroll test now states both corners rather than one number.
2026-09-13 21:29:38 -04:00
iris 3a74a04a5b Post task updates to the loop instead of waking it
`WakeTaskQueue` becomes `TaskQueue`, which carries the update itself.
Delivery and waking are then one act: the winit host sends it through the
`EventLoopProxy` as a `DefaultEvent::Update`, so there is no channel
beside the loop and nothing has to claim a redraw is needed in order to be
looked at. `Window::request_redraw` is gone from this path; `event`
applies the update and then asks the tree whether anything became dirty,
which is the same question `window_event` already ended with -- now
`schedule_redraw`, called from both.

The loop's message type is `DefaultEvent<State>`, so `Proxy` becomes a
wrapper that takes the application's own `Event` and requires it to be
`Send`, since it now crosses to the task thread by that route.

The harness supplies a channel-backed queue, which is what lets a test
hold updates until it asks for them.

Tests split by subject -- layout, pointer, scroll, tasks -- with the
region helper in `tests/common`.
2026-09-13 21:04:29 -04:00
iris e97aba30e0 Merge upstream/main (#14) into split/15-harness 2026-09-13 20:44:01 -04:00
iris-aiandiris 32b10383d8 Rename the Sized widget to SetSize (#14)
`Sized` 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. It was already biting inside iris: `default/mod.rs`, `widget/ptr.rs` and `widget/text/build.rs` all imported `std::marker::Sized` explicitly to get out from under it, which they no longer need.

`SetSize` rather than `FixedSize` because the size it sets need not be fixed -- `width(rest(2))` (a flex weight) and `width(rel(0.5))` (half the parent) build the same widget, and both are more common than `sized((100, 100))`. It also pairs with the `MaxSize` beside it in that module: one sets a length, the other caps it. The builders are unchanged.

`tests/prelude_bounds.rs` is a compile-level guard -- it fails to build if the prelude shadows `Sized` again, which I checked by reverting `src/` under it:

```
error[E0404]: expected trait, found struct `Sized`
 --> tests/prelude_bounds.rs:8:22
  |
8 | fn takes_unsized<T: ?Sized>(_: &T) {}
  |                      ^^^^^ not a trait
```

The pad tab of the tabs example -- the one built out of `sized` and the flexible widths -- renders pixel-identical to before the rename.

---------

Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: iris/iris#14
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
2026-09-13 20:16:17 -04:00
iris d5efdd2b97 Run a ui without a window, and test one
`Tasks` held an `Arc<Window>` only to call `request_redraw` when a task
finished, which made the task queue -- and so `DefaultRsc` -- impossible
to build without a window. It now takes an `Arc<dyn WakeTaskQueue>`, and
`Window` implements it.

Waking moves from "the task ended" to "an update was sent", which is when
there is something for the host to apply: a task that keeps running after
sending one no longer holds its update until it finishes, and a task that
sends none no longer asks for a frame it does not need.

`iris::harness` is what that buys. `UiRenderState` already does layout,
hit testing and primitive building with no surface, so a test can build a
tree, run frames, move a pointer and read back where widgets landed.
`tests/harness.rs` does each of those; none of them could be written
before, since the only entry point to layout was a window.

It does not draw. A claim about pixels still needs a real surface.
2026-09-13 20:14:11 -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
42 changed files with 2056 additions and 1238 deletions

No files matched your search

Generated
+203 -229
View File
@@ -234,18 +234,18 @@ dependencies = [
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.8.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d"
dependencies = [ dependencies = [
"bit-vec", "bit-vec",
] ]
[[package]] [[package]]
name = "bit-vec" name = "bit-vec"
version = "0.8.0" version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
[[package]] [[package]]
name = "bit_field" name = "bit_field"
@@ -274,12 +274,6 @@ dependencies = [
"core2", "core2",
] ]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]] [[package]]
name = "block2" name = "block2"
version = "0.5.1" version = "0.5.1"
@@ -289,6 +283,15 @@ dependencies = [
"objc2 0.5.2", "objc2 0.5.2",
] ]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2 0.6.4",
]
[[package]] [[package]]
name = "built" name = "built"
version = "0.8.0" version = "0.8.0"
@@ -391,9 +394,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]] [[package]]
name = "cfg_aliases" name = "cfg_aliases"
version = "0.2.1" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]] [[package]]
name = "clipboard-win" name = "clipboard-win"
@@ -406,9 +409,9 @@ dependencies = [
[[package]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.12.0" version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [ dependencies = [
"serde", "serde",
"termcolor", "termcolor",
@@ -450,16 +453,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@@ -473,8 +466,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation 0.9.4", "core-foundation",
"core-graphics-types 0.1.3", "core-graphics-types",
"foreign-types", "foreign-types",
"libc", "libc",
] ]
@@ -486,18 +479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation 0.9.4", "core-foundation",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.10.0",
"core-foundation 0.10.1",
"libc", "libc",
] ]
@@ -739,15 +721,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "font-types"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "font-types" name = "font-types"
version = "0.12.4" version = "0.12.4"
@@ -771,7 +744,7 @@ dependencies = [
"objc2-core-text", "objc2-core-text",
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
"parlance", "parlance",
"read-fonts 0.41.0", "read-fonts",
"roxmltree", "roxmltree",
"smallvec", "smallvec",
"windows", "windows",
@@ -806,6 +779,30 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]] [[package]]
name = "fxhash" name = "fxhash"
version = "0.2.1" version = "0.2.1"
@@ -860,9 +857,9 @@ dependencies = [
[[package]] [[package]]
name = "glow" name = "glow"
version = "0.16.0" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"slotmap", "slotmap",
@@ -893,26 +890,6 @@ dependencies = [
"windows", "windows",
] ]
[[package]]
name = "gpu-descriptor"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
dependencies = [
"bitflags 2.10.0",
"gpu-descriptor-types",
"hashbrown 0.15.5",
]
[[package]]
name = "gpu-descriptor-types"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91"
dependencies = [
"bitflags 2.10.0",
]
[[package]] [[package]]
name = "half" name = "half"
version = "2.7.1" version = "2.7.1"
@@ -933,7 +910,7 @@ checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"bytemuck", "bytemuck",
"read-fonts 0.41.0", "read-fonts",
"smallvec", "smallvec",
] ]
@@ -972,12 +949,6 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hexf-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.3.0" version = "2.3.0"
@@ -1108,9 +1079,9 @@ checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad"
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.9" version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
@@ -1126,8 +1097,8 @@ dependencies = [
"rayon", "rayon",
"rgb", "rgb",
"tiff", "tiff",
"zune-core 0.5.0", "zune-core",
"zune-jpeg 0.5.8", "zune-jpeg",
] ]
[[package]] [[package]]
@@ -1247,11 +1218,12 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.83" version = "0.3.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
dependencies = [ dependencies = [
"once_cell", "cfg-if",
"futures-util",
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -1280,9 +1252,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.179" version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]] [[package]]
name = "libfuzzer-sys" name = "libfuzzer-sys"
@@ -1375,15 +1347,6 @@ dependencies = [
"imgref", "imgref",
] ]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "maybe-rayon" name = "maybe-rayon"
version = "0.1.1" version = "0.1.1"
@@ -1409,21 +1372,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "metal"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15"
dependencies = [
"bitflags 2.10.0",
"block",
"core-graphics-types 0.2.0",
"foreign-types",
"log",
"objc",
"paste",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -1436,9 +1384,9 @@ dependencies = [
[[package]] [[package]]
name = "moxcms" name = "moxcms"
version = "0.7.11" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [ dependencies = [
"num-traits", "num-traits",
"pxfm", "pxfm",
@@ -1446,9 +1394,9 @@ dependencies = [
[[package]] [[package]]
name = "naga" name = "naga"
version = "28.0.0" version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135" checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bit-set", "bit-set",
@@ -1457,11 +1405,11 @@ dependencies = [
"cfg_aliases", "cfg_aliases",
"codespan-reporting", "codespan-reporting",
"half", "half",
"hashbrown 0.16.1", "hashbrown 0.17.1",
"hexf-parse",
"indexmap", "indexmap",
"libm", "libm",
"log", "log",
"naga-types",
"num-traits", "num-traits",
"once_cell", "once_cell",
"rustc-hash", "rustc-hash",
@@ -1470,6 +1418,18 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "naga-types"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49"
dependencies = [
"hashbrown 0.17.1",
"indexmap",
"rustc-hash",
"thiserror 2.0.17",
]
[[package]] [[package]]
name = "ndk" name = "ndk"
version = "0.9.0" version = "0.9.0"
@@ -1594,15 +1554,6 @@ dependencies = [
"syn 2.0.113", "syn 2.0.113",
] ]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]] [[package]]
name = "objc-sys" name = "objc-sys"
version = "0.3.5" version = "0.3.5"
@@ -1635,13 +1586,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2", "block2 0.5.1",
"libc", "libc",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-data", "objc2-core-data",
"objc2-core-image", "objc2-core-image",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-quartz-core", "objc2-quartz-core 0.2.2",
] ]
[[package]] [[package]]
@@ -1663,7 +1614,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-location", "objc2-core-location",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -1675,7 +1626,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -1687,7 +1638,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -1722,10 +1673,10 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-metal", "objc2-metal 0.2.2",
] ]
[[package]] [[package]]
@@ -1734,7 +1685,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-contacts", "objc2-contacts",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -1763,7 +1714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2", "block2 0.5.1",
"dispatch", "dispatch",
"libc", "libc",
"objc2 0.5.2", "objc2 0.5.2",
@@ -1797,7 +1748,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-app-kit 0.2.2", "objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -1810,11 +1761,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
[[package]]
name = "objc2-metal"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
"objc2 0.6.4",
"objc2-foundation 0.3.2",
]
[[package]] [[package]]
name = "objc2-quartz-core" name = "objc2-quartz-core"
version = "0.2.2" version = "0.2.2"
@@ -1822,10 +1785,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.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]] [[package]]
@@ -1845,7 +1822,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-cloud-kit", "objc2-cloud-kit",
"objc2-core-data", "objc2-core-data",
@@ -1853,7 +1830,7 @@ dependencies = [
"objc2-core-location", "objc2-core-location",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-link-presentation", "objc2-link-presentation",
"objc2-quartz-core", "objc2-quartz-core 0.2.2",
"objc2-symbols", "objc2-symbols",
"objc2-uniform-type-identifiers", "objc2-uniform-type-identifiers",
"objc2-user-notifications", "objc2-user-notifications",
@@ -1865,7 +1842,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -1877,7 +1854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-location", "objc2-core-location",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -1971,7 +1948,7 @@ dependencies = [
"linebender_resource_handle", "linebender_resource_handle",
"parlance", "parlance",
"parley_data", "parley_data",
"skrifa 0.44.0", "skrifa",
] ]
[[package]] [[package]]
@@ -2275,9 +2252,9 @@ dependencies = [
[[package]] [[package]]
name = "ravif" name = "ravif"
version = "0.12.0" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
dependencies = [ dependencies = [
"avif-serialize", "avif-serialize",
"imgref", "imgref",
@@ -2294,6 +2271,18 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "raw-window-metal"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
dependencies = [
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"objc2-quartz-core 0.3.2",
]
[[package]] [[package]]
name = "rayon" name = "rayon"
version = "1.11.0" version = "1.11.0"
@@ -2314,16 +2303,6 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "read-fonts"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eaa2941a4c05443ee3a7b26ab076a553c343ad5995230cc2b1d3e993bdc6345"
dependencies = [
"bytemuck",
"font-types 0.10.1",
]
[[package]] [[package]]
name = "read-fonts" name = "read-fonts"
version = "0.41.0" version = "0.41.0"
@@ -2331,7 +2310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"font-types 0.12.4", "font-types",
"once_cell", "once_cell",
] ]
@@ -2506,16 +2485,6 @@ dependencies = [
"quote", "quote",
] ]
[[package]]
name = "skrifa"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9eb0b904a04d09bd68c65d946617b8ff733009999050f3b851c32fb3cfb60e"
dependencies = [
"bytemuck",
"read-fonts 0.36.0",
]
[[package]] [[package]]
name = "skrifa" name = "skrifa"
version = "0.44.0" version = "0.44.0"
@@ -2523,7 +2492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"read-fonts 0.41.0", "read-fonts",
] ]
[[package]] [[package]]
@@ -2583,9 +2552,9 @@ dependencies = [
[[package]] [[package]]
name = "spirv" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
] ]
@@ -2614,7 +2583,7 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e"
dependencies = [ dependencies = [
"skrifa 0.39.0", "skrifa",
"yazi", "yazi",
"zeno", "zeno",
] ]
@@ -2703,16 +2672,16 @@ dependencies = [
[[package]] [[package]]
name = "tiff" name = "tiff"
version = "0.10.3" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
dependencies = [ dependencies = [
"fax", "fax",
"flate2", "flate2",
"half", "half",
"quick-error", "quick-error",
"weezl", "weezl",
"zune-jpeg 0.4.21", "zune-jpeg",
] ]
[[package]] [[package]]
@@ -2885,9 +2854,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.106" version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -2898,22 +2867,19 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.56" version = "0.4.78"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
dependencies = [ dependencies = [
"cfg-if",
"js-sys", "js-sys",
"once_cell",
"wasm-bindgen", "wasm-bindgen",
"web-sys",
] ]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.106" version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -2921,22 +2887,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.106" version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 3.0.5",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.106" version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -3052,9 +3018,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.83" version = "0.3.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -3078,9 +3044,9 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]] [[package]]
name = "wgpu" name = "wgpu"
version = "28.0.0" version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f" checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bitflags 2.10.0", "bitflags 2.10.0",
@@ -3088,7 +3054,7 @@ dependencies = [
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"document-features", "document-features",
"hashbrown 0.16.1", "hashbrown 0.17.1",
"js-sys", "js-sys",
"log", "log",
"naga", "naga",
@@ -3108,9 +3074,9 @@ dependencies = [
[[package]] [[package]]
name = "wgpu-core" name = "wgpu-core"
version = "28.0.0" version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb4c8b5db5f00e56f1f08869d870a0dff7c8bc7ebc01091fec140b0cf0211a9" checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bit-set", "bit-set",
@@ -3119,10 +3085,11 @@ dependencies = [
"bytemuck", "bytemuck",
"cfg_aliases", "cfg_aliases",
"document-features", "document-features",
"hashbrown 0.16.1", "hashbrown 0.17.1",
"indexmap", "indexmap",
"log", "log",
"naga", "naga",
"naga-types",
"once_cell", "once_cell",
"parking_lot", "parking_lot",
"portable-atomic", "portable-atomic",
@@ -3135,66 +3102,70 @@ dependencies = [
"wgpu-core-deps-emscripten", "wgpu-core-deps-emscripten",
"wgpu-core-deps-windows-linux-android", "wgpu-core-deps-windows-linux-android",
"wgpu-hal", "wgpu-hal",
"wgpu-naga-bridge",
"wgpu-types", "wgpu-types",
] ]
[[package]] [[package]]
name = "wgpu-core-deps-apple" name = "wgpu-core-deps-apple"
version = "28.0.0" version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da" checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68"
dependencies = [ dependencies = [
"wgpu-hal", "wgpu-hal",
] ]
[[package]] [[package]]
name = "wgpu-core-deps-emscripten" name = "wgpu-core-deps-emscripten"
version = "28.0.0" version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79" checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86"
dependencies = [ dependencies = [
"wgpu-hal", "wgpu-hal",
] ]
[[package]] [[package]]
name = "wgpu-core-deps-windows-linux-android" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae" checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f"
dependencies = [ dependencies = [
"wgpu-hal", "wgpu-hal",
] ]
[[package]] [[package]]
name = "wgpu-hal" name = "wgpu-hal"
version = "28.0.0" version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "293080d77fdd14d6b08a67c5487dfddbf874534bb7921526db56a7b75d7e3bef" checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"arrayvec", "arrayvec",
"ash", "ash",
"bit-set", "bit-set",
"bitflags 2.10.0", "bitflags 2.10.0",
"block", "block2 0.6.2",
"bytemuck", "bytemuck",
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"core-graphics-types 0.2.0",
"glow", "glow",
"glutin_wgl_sys", "glutin_wgl_sys",
"gpu-allocator", "gpu-allocator",
"gpu-descriptor", "hashbrown 0.17.1",
"hashbrown 0.16.1",
"js-sys", "js-sys",
"khronos-egl", "khronos-egl",
"libc", "libc",
"libloading", "libloading",
"log", "log",
"metal",
"naga", "naga",
"naga-types",
"ndk-sys", "ndk-sys",
"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", "once_cell",
"ordered-float", "ordered-float",
"parking_lot", "parking_lot",
@@ -3203,26 +3174,44 @@ dependencies = [
"profiling", "profiling",
"range-alloc", "range-alloc",
"raw-window-handle", "raw-window-handle",
"raw-window-metal",
"renderdoc-sys", "renderdoc-sys",
"smallvec", "smallvec",
"static_assertions",
"thiserror 2.0.17", "thiserror 2.0.17",
"wasm-bindgen", "wasm-bindgen",
"wayland-sys",
"web-sys", "web-sys",
"wgpu-naga-bridge",
"wgpu-types", "wgpu-types",
"windows", "windows",
"windows-core", "windows-core",
"windows-result",
]
[[package]]
name = "wgpu-naga-bridge"
version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081"
dependencies = [
"naga",
"wgpu-types",
] ]
[[package]] [[package]]
name = "wgpu-types" name = "wgpu-types"
version = "28.0.0" version = "30.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c" checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"bytemuck", "bytemuck",
"js-sys", "js-sys",
"log", "log",
"naga-types",
"raw-window-handle",
"static_assertions",
"web-sys", "web-sys",
] ]
@@ -3586,12 +3575,12 @@ dependencies = [
"android-activity", "android-activity",
"atomic-waker", "atomic-waker",
"bitflags 2.10.0", "bitflags 2.10.0",
"block2", "block2 0.5.1",
"bytemuck", "bytemuck",
"calloop", "calloop",
"cfg_aliases", "cfg_aliases",
"concurrent-queue", "concurrent-queue",
"core-foundation 0.9.4", "core-foundation",
"core-graphics", "core-graphics",
"cursor-icon", "cursor-icon",
"dpi", "dpi",
@@ -3858,12 +3847,6 @@ dependencies = [
"syn 3.0.5", "syn 3.0.5",
] ]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]] [[package]]
name = "zune-core" name = "zune-core"
version = "0.5.0" version = "0.5.0"
@@ -3879,20 +3862,11 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "zune-jpeg"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
dependencies = [
"zune-core 0.4.12",
]
[[package]] [[package]]
name = "zune-jpeg" name = "zune-jpeg"
version = "0.5.8" version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5"
dependencies = [ dependencies = [
"zune-core 0.5.0", "zune-core",
] ]
+2 -2
View File
@@ -29,9 +29,9 @@ edition = "2024"
[workspace.dependencies] [workspace.dependencies]
pollster = "0.4.0" pollster = "0.4.0"
winit = "0.30.12" winit = "0.30.12"
wgpu = "28.0.0" wgpu = "30.0.1"
bytemuck = "1.23.1" bytemuck = "1.23.1"
image = "0.25.6" image = "0.25.10"
parley = "0.11.1" parley = "0.11.1"
swash = "0.2.10" swash = "0.2.10"
fxhash = "0.2.1" fxhash = "0.2.1"
+1 -1
View File
@@ -421,7 +421,7 @@ impl Display for UiRegion {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct PixelRegion { pub struct PixelRegion {
pub top_left: Vec2, pub top_left: Vec2,
pub bot_right: Vec2, pub bot_right: Vec2,
+3 -3
View File
@@ -1,7 +1,7 @@
use std::ops::{Index, IndexMut}; use std::ops::{Index, IndexMut};
use crate::{ use crate::{
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives}, render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
util::to_mut, util::to_mut,
}; };
@@ -39,7 +39,7 @@ struct Child {
tail: usize, tail: usize,
} }
pub type PrimitiveLayers = Layers<Primitives>; pub type DrawLayers = Layers<LayerDraws>;
impl<T: Default> Layers<T> { impl<T: Default> Layers<T> {
pub fn new() -> 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>( pub fn write<P: Primitive>(
&mut self, &mut self,
layer: LayerId, layer: LayerId,
+13 -22
View File
@@ -1,6 +1,5 @@
use crate::{ use crate::{
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
util::Vec2,
}; };
use parley::{ use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
@@ -159,7 +158,7 @@ impl TextBuffer {
} }
impl TextData { 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(); let mut placed = Vec::new();
for line in buffer.layout.lines() { for line in buffer.layout.lines() {
for item in line.items() { for item in line.items() {
@@ -185,17 +184,14 @@ impl TextData {
subpixel, subpixel,
coords: coords_hash, coords: coords_hash,
}; };
let Some(entry) = self.glyph_entry( let Some(entry) = self.glyph_entry(GlyphRaster {
GlyphRaster { key,
key, font: font_ref,
font: font_ref, font_size,
font_size, coords,
coords, subpixel,
subpixel, glyph_id: glyph.id,
glyph_id: glyph.id, }) else {
},
textures,
) else {
continue; continue;
}; };
placed.push(PlacedGlyph { placed.push(PlacedGlyph {
@@ -211,11 +207,7 @@ impl TextData {
placed placed
} }
fn glyph_entry( fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option<GlyphEntry> {
&mut self,
glyph: GlyphRaster<'_>,
textures: &mut Textures,
) -> Option<GlyphEntry> {
if let Some(entry) = self.atlas.get(&glyph.key) { if let Some(entry) = self.atlas.get(&glyph.key) {
return entry; return entry;
} }
@@ -237,7 +229,7 @@ impl TextData {
.render(&mut scaler, glyph.glyph_id as u16); .render(&mut scaler, glyph.glyph_id as u16);
if let Some(image) = image { if let Some(image) = image {
self.atlas.insert(glyph.key, &image, textures) self.atlas.insert(glyph.key, &image)
} else { } else {
self.atlas.insert_empty(glyph.key); self.atlas.insert_empty(glyph.key);
None None
@@ -278,10 +270,9 @@ impl TextData {
buffer: &mut TextBuffer, buffer: &mut TextBuffer,
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, width: Option<f32>,
textures: &mut Textures,
) -> RenderedText { ) -> RenderedText {
buffer.shape(self, attrs, width); buffer.shape(self, attrs, width);
let glyphs = self.place(buffer, textures); let glyphs = self.place(buffer);
RenderedText { RenderedText {
glyphs, glyphs,
size: buffer.size(), size: buffer.size(),
+18 -19
View File
@@ -1,7 +1,4 @@
use crate::{ use crate::util::{RefCounter, Vec2};
render::TexturePrimitive,
util::{RefCounter, Vec2},
};
use image::{DynamicImage, GenericImageView}; use image::{DynamicImage, GenericImageView};
use std::{ use std::{
ops::Index, ops::Index,
@@ -10,7 +7,7 @@ use std::{
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TextureHandle { pub struct TextureHandle {
inner: TexturePrimitive, slot: u32,
size: Vec2, size: Vec2,
counter: RefCounter, counter: RefCounter,
send: Sender<u32>, send: Sender<u32>,
@@ -31,6 +28,8 @@ pub enum TextureUpdate<'a> {
Set(u32, &'a DynamicImage), Set(u32, &'a DynamicImage),
Patch(u32, PatchRect, &'a DynamicImage), Patch(u32, PatchRect, &'a DynamicImage),
Free(u32), Free(u32),
/// Added and freed before the renderer drained either update. It still has
/// to push a slot to stay lined up with `images`; `Free` then empties it.
PushFree, PushFree,
SetFree, SetFree,
} }
@@ -64,14 +63,8 @@ impl Textures {
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle { pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
let image = image.into(); let image = image.into();
let size = image.dimensions().into(); let size = image.dimensions().into();
let view_idx = self.push(image);
// 0 == default in renderer; TODO: actually create samplers here
let sampler_idx = 0;
TextureHandle { TextureHandle {
inner: TexturePrimitive { slot: self.push(image),
view_idx,
sampler_idx,
},
size, size,
counter: RefCounter::new(), counter: RefCounter::new(),
send: self.send.clone(), send: self.send.clone(),
@@ -92,15 +85,20 @@ impl Textures {
} }
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage { 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() .as_mut()
.expect("texture was freed while still held") .expect("texture was freed while still held")
} }
/// Queue an upload of just `rect`, after writing it with `image_mut`. /// Queue an upload of just `rect`, after writing it with `image_mut`.
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) { pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
self.updates self.updates.push(Update::Patch(handle.slot, rect));
.push(Update::Patch(handle.inner.view_idx, rect)); }
/// How many textures are live, which is what a ui can ask; the renderer's
/// copies follow from the updates it drains.
pub fn count(&self) -> usize {
self.images.iter().flatten().count()
} }
pub fn free(&mut self) { pub fn free(&mut self) {
@@ -131,8 +129,9 @@ impl Textures {
} }
impl TextureHandle { impl TextureHandle {
pub fn primitive(&self) -> TexturePrimitive { /// Index into `Textures`, and into the renderer's parallel slots.
self.inner pub fn slot(&self) -> u32 {
self.slot
} }
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
self.size self.size
@@ -142,7 +141,7 @@ impl TextureHandle {
impl Drop for TextureHandle { impl Drop for TextureHandle {
fn drop(&mut self) { fn drop(&mut self) {
if self.counter.drop() { if self.counter.drop() {
let _ = self.send.send(self.inner.view_idx); let _ = self.send.send(self.slot);
} }
} }
} }
@@ -151,7 +150,7 @@ impl Index<&TextureHandle> for Textures {
type Output = DynamicImage; type Output = DynamicImage;
fn index(&self, index: &TextureHandle) -> &Self::Output { 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::{ use crate::{
PatchRect, TextureHandle, Textures, PatchRect,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
use image::RgbaImage; use image::RgbaImage;
use swash::scale::image::{Content, Image}; 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 /// Transparent margin kept around every glyph, so that sampling one cannot
/// pick up its neighbour along a shared edge. /// pick up its neighbour along a shared edge.
@@ -35,8 +36,8 @@ pub struct GlyphEntry {
pub width: u32, pub width: u32,
pub height: u32, pub height: u32,
pub is_colored: bool, pub is_colored: bool,
pub view_idx: u32, /// Which atlas array layer this glyph is on.
pub sampler_idx: u32, pub layer: u32,
} }
impl GlyphEntry { impl GlyphEntry {
@@ -48,18 +49,26 @@ impl GlyphEntry {
} }
struct Page { struct Page {
handle: TextureHandle, image: RgbaImage,
x: u32, x: u32,
y: u32, y: u32,
shelf_height: u32, shelf_height: u32,
} }
/// A rectangle of one page the renderer has not uploaded yet.
#[derive(Clone, Copy)]
pub struct PageUpload {
pub layer: u32,
pub rect: PatchRect,
}
#[derive(Default)] #[derive(Default)]
pub struct GlyphAtlas { pub struct GlyphAtlas {
pages: Vec<Page>, pages: Vec<Page>,
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached /// `None` for a glyph that rasterised to nothing -- a space, say. Cached
/// too, so it is not re-rasterised on every layout. /// too, so it is not re-rasterised on every layout.
entries: HashMap<GlyphKey, Option<GlyphEntry>>, entries: HashMap<GlyphKey, Option<GlyphEntry>>,
uploads: Vec<PageUpload>,
} }
impl GlyphAtlas { impl GlyphAtlas {
@@ -67,12 +76,7 @@ impl GlyphAtlas {
self.entries.get(key).copied() self.entries.get(key).copied()
} }
pub fn insert( pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option<GlyphEntry> {
&mut self,
key: GlyphKey,
image: &Image,
textures: &mut Textures,
) -> Option<GlyphEntry> {
let w = image.placement.width; let w = image.placement.width;
let h = image.placement.height; let h = image.placement.height;
if w == 0 || h == 0 { if w == 0 || h == 0 {
@@ -94,23 +98,11 @@ impl GlyphAtlas {
return None; return None;
} }
let (page_idx, x, y) = self.allocate(w, h, textures); let upload = self.allocate(w, h);
let page = &self.pages[page_idx]; 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 scale = 1.0 / PAGE as f32;
let entry = GlyphEntry { let entry = GlyphEntry {
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale), uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
@@ -120,39 +112,60 @@ impl GlyphAtlas {
width: w, width: w,
height: h, height: h,
is_colored: matches!(image.content, Content::Color), is_colored: matches!(image.content, Content::Color),
view_idx: page.handle.primitive().view_idx, layer: upload.layer,
sampler_idx: page.handle.primitive().sampler_idx,
}; };
self.entries.insert(key, Some(entry)); self.entries.insert(key, Some(entry));
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 if let Some((i, (x, y))) = self
.pages .pages
.iter_mut() .iter_mut()
.enumerate() .enumerate()
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position))) .find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
{ {
return (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 { self.pages.push(Page {
handle, image: RgbaImage::new(PAGE, PAGE),
x: PAD + w + PAD, x: PAD + w + PAD,
y: PAD, y: PAD,
shelf_height: h + 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) { pub fn insert_empty(&mut self, key: GlyphKey) {
self.entries.insert(key, None); self.entries.insert(key, None);
} }
pub fn page_count(&self) -> usize { pub fn page_count(&self) -> u32 {
self.pages.len() self.pages.len() as u32
} }
pub fn glyph_count(&self) -> usize { pub fn glyph_count(&self) -> usize {
+1 -5
View File
@@ -12,20 +12,16 @@ pub struct WindowUniform {
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PrimitiveInstance { pub struct PrimitiveInstance {
pub region: UiRegion, pub region: UiRegion,
pub binding: u32,
pub idx: u32,
pub mask_idx: MaskIdx, pub mask_idx: MaskIdx,
} }
impl PrimitiveInstance { impl PrimitiveInstance {
const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![ const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
0 => Float32x2, 0 => Float32x2,
1 => Float32x2, 1 => Float32x2,
2 => Float32x2, 2 => Float32x2,
3 => Float32x2, 3 => Float32x2,
4 => Uint32, 4 => Uint32,
5 => Uint32,
6 => Uint32,
]; ];
pub fn desc() -> VertexBufferLayout<'static> { pub fn desc() -> VertexBufferLayout<'static> {
+202 -193
View File
@@ -1,8 +1,6 @@
use std::num::NonZero;
use crate::{ use crate::{
UiData, UiRenderState, UiData, UiRenderState,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf}, render::{data::PrimitiveInstance, util::ArrBuf},
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
use data::WindowUniform; use data::WindowUniform;
@@ -13,6 +11,7 @@ use wgpu::{
mod atlas; mod atlas;
mod data; mod data;
mod page;
mod primitive; mod primitive;
mod texture; mod texture;
mod util; mod util;
@@ -21,42 +20,63 @@ pub use atlas::*;
pub use data::{Mask, MaskIdx}; pub use data::{Mask, MaskIdx};
pub use primitive::*; pub use primitive::*;
const SHAPE_SHADER: &str = include_str!("./shader.wgsl"); const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
pub struct UiRenderNode { pub struct UiRenderNode {
uniform_group: BindGroup, shared_layout: BindGroupLayout,
primitive_layout: BindGroupLayout, shared_group: BindGroup,
rsc_layout: BindGroupLayout, format: TextureFormat,
rsc_group: BindGroup,
pipeline: RenderPipeline, /// One per registered primitive, in id order.
primitives: Vec<PrimitivePipeline>,
layers: HashMap<usize, RenderLayer>, layers: HashMap<usize, RenderLayer>,
active: Vec<usize>, active: Vec<usize>,
window_buffer: Buffer, window_buffer: Buffer,
textures: GpuTextures,
masks: ArrBuf<Mask>, masks: ArrBuf<Mask>,
} }
struct RenderLayer { struct RenderLayer {
/// One per registered primitive, `None` where this layer draws none.
primitives: Vec<Option<ListBuffers>>,
}
/// What draws one registered primitive.
struct PrimitivePipeline {
data_layout: BindGroupLayout,
pipeline: RenderPipeline,
render: Box<dyn PrimitiveRender>,
}
/// One list's vertex buffer and the data its shader reads.
struct ListBuffers {
instance: ArrBuf<PrimitiveInstance>, instance: ArrBuf<PrimitiveInstance>,
primitives: PrimitiveBuffers, data: ArrBuf<u8>,
primitive_group: BindGroup, group: Option<BindGroup>,
/// What the primitive asked to keep per instance, if anything.
bindings: Vec<u32>,
} }
impl UiRenderNode { impl UiRenderNode {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) { pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.shared_group, &[]);
pass.set_bind_group(0, &self.uniform_group, &[]);
pass.set_bind_group(2, &self.rsc_group, &[]);
for i in &self.active { for i in &self.active {
let layer = &self.layers[i]; let layer = &self.layers[i];
if layer.instance.len() == 0 { for (id, list) in layer.primitives.iter().enumerate() {
continue; 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: &mut UiData,
ui_render: &mut UiRenderState, ui_render: &mut UiRenderState,
) { ) {
// Before the layers: each list is given its pipeline's data layout.
self.build_pipelines(device, queue, &ui.primitives);
self.active.clear(); self.active.clear();
for (i, primitives) in ui_render.layers.iter_mut() { for (i, draws) in ui_render.layers.iter_mut() {
self.active.push(i); 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) { if let Some(inst) = ui_render.active.get_mut(&change.id) {
for h in &mut inst.primitives { for h in &mut inst.primitives {
if h.layer == i && h.inst_idx == change.old { if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
h.inst_idx = change.new; h.inst_idx = change.new;
break; break;
} }
} }
} }
} }
let rlayer = self.layers.entry(i).or_insert_with(|| { let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new);
let primitives = PrimitiveBuffers::new(device); if draws.updated {
let primitive_group = let lists = draws.primitives();
Self::primitive_group(device, &self.primitive_layout, primitives.buffers()); // The zip would otherwise skip a list with no pipeline.
RenderLayer { assert!(lists.len() <= self.primitives.len());
instance: ArrBuf::new( rlayer.primitives.resize_with(lists.len(), || None);
device, for ((buffers, list), primitive) in rlayer
BufferUsages::VERTEX | BufferUsages::COPY_DST, .primitives
"instance", .iter_mut()
), .zip(lists)
primitives, .zip(&self.primitives)
primitive_group, {
let Some(list) = list else {
continue;
};
buffers
.get_or_insert_with(|| ListBuffers::new(device))
.update(device, queue, primitive, list);
} }
}); draws.updated = false;
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;
} }
} }
let mut changed = false; for primitive in &mut self.primitives {
changed |= self.textures.update(&mut ui.textures); primitive.render.update(ui);
}
if ui.masks.changed { if ui.masks.changed {
ui.masks.changed = false; ui.masks.changed = false;
self.masks.update(device, queue, &ui.masks[..]); if self.masks.update(device, queue, &ui.masks[..]) {
changed = true; self.shared_group = Self::shared_group(
} device,
if changed { &self.shared_layout,
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks); &self.window_buffer,
&self.masks,
);
}
} }
} }
@@ -128,17 +149,7 @@ impl UiRenderNode {
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
} }
pub fn new( pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
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()),
});
let window_uniform = WindowUniform { let window_uniform = WindowUniform {
width: config.width as f32, width: config.width as f32,
height: config.height as f32, height: config.height as f32,
@@ -149,67 +160,73 @@ impl UiRenderNode {
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
}); });
let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor { let shared_layout = Self::shared_layout(device);
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 masks = ArrBuf::new( let masks = ArrBuf::new(
device, device,
BufferUsages::STORAGE | BufferUsages::COPY_DST, BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks", "ui masks",
); );
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
let rsc_layout = Self::rsc_layout(device, &limits); Self {
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks); 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 { /// Compiles a pipeline for every primitive registered since the last call.
label: Some("UI Shape Pipeline Layout"), /// Sources only ever arrive at the end, so an id keeps its pipeline.
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout], fn build_pipelines(&mut self, device: &Device, queue: &Queue, registry: &PrimitiveRegistry) {
immediate_size: 0, 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 = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
self.primitives.push(PrimitivePipeline {
data_layout,
pipeline,
render,
});
}
}
fn pipeline(
device: &Device,
layout: &PipelineLayout,
format: TextureFormat,
wgsl: &str,
label: &str,
) -> RenderPipeline {
let module = device.create_shader_module(ShaderModuleDescriptor {
label: Some(label),
source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()),
}); });
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("UI Shape Pipeline"), label: Some(label),
layout: Some(&pipeline_layout), layout: Some(layout),
vertex: VertexState { vertex: VertexState {
module: &shader, module: &module,
entry_point: Some("vs_main"), entry_point: Some("vs_main"),
buffers: &[PrimitiveInstance::desc()], buffers: &[Some(PrimitiveInstance::desc())],
compilation_options: Default::default(), compilation_options: Default::default(),
}, },
fragment: Some(FragmentState { fragment: Some(FragmentState {
module: &shader, module: &module,
entry_point: Some("fs_main"), entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState { targets: &[Some(ColorTargetState {
format: config.format, format,
blend: Some(BlendState::ALPHA_BLENDING), blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL, write_mask: ColorWrites::ALL,
})], })],
@@ -232,90 +249,42 @@ impl UiRenderNode {
}, },
multiview_mask: None, multiview_mask: None,
cache: None, cache: None,
});
Self {
uniform_group,
primitive_layout,
rsc_layout,
rsc_group,
pipeline,
window_buffer,
layers: HashMap::default(),
active: Vec::new(),
textures: tex_manager,
masks,
}
}
fn bind_group_0(
device: &Device,
layout: &BindGroupLayout,
window_buffer: &Buffer,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[BindGroupEntry {
binding: 0,
resource: window_buffer.as_entire_binding(),
}],
label: Some("ui window"),
}) })
} }
fn primitive_group( /// What every draw in the ui is given: the window and the masks.
device: &Device, fn shared_layout(device: &Device) -> BindGroupLayout {
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 {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[
BindGroupLayoutEntry { BindGroupLayoutEntry {
binding: 0, binding: 0,
visibility: ShaderStages::FRAGMENT, visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Texture { ty: BindingType::Buffer {
sample_type: TextureSampleType::Float { filterable: false }, ty: BufferBindingType::Uniform,
view_dimension: TextureViewDimension::D2, has_dynamic_offset: false,
multisampled: false, min_binding_size: BufferSize::new(size_of::<WindowUniform>() as u64),
}, },
count: Some(NonZero::new(limits.max_textures).unwrap()), count: None,
}, },
BindGroupLayoutEntry { BindGroupLayoutEntry {
binding: 1, binding: 1,
visibility: ShaderStages::FRAGMENT, 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: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true }, ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false, has_dynamic_offset: false,
min_binding_size: None, min_binding_size: BufferSize::new(size_of::<Mask>() as u64),
}, },
count: None, count: None,
}, },
], ],
label: Some("ui rsc"), label: Some("ui shared"),
}) })
} }
fn rsc_group( fn shared_group(
device: &Device, device: &Device,
layout: &BindGroupLayout, layout: &BindGroupLayout,
tex_manager: &GpuTextures, window: &Buffer,
masks: &ArrBuf<Mask>, masks: &ArrBuf<Mask>,
) -> BindGroup { ) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor { device.create_bind_group(&BindGroupDescriptor {
@@ -323,45 +292,85 @@ impl UiRenderNode {
entries: &[ entries: &[
BindGroupEntry { BindGroupEntry {
binding: 0, binding: 0,
resource: BindingResource::TextureViewArray(&tex_manager.views()), resource: window.as_entire_binding(),
}, },
BindGroupEntry { BindGroupEntry {
binding: 1, binding: 1,
resource: BindingResource::SamplerArray(&tex_manager.samplers()),
},
BindGroupEntry {
binding: 2,
resource: masks.buffer.as_entire_binding(), resource: masks.buffer.as_entire_binding(),
}, },
], ],
label: Some("ui rsc"), label: Some("ui shared"),
}) })
} }
pub fn view_count(&self) -> usize { /// Layout for a list of one primitive's data. Every size in the ui is
self.textures.view_count() /// 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 { impl RenderLayer {
max_textures: u32, fn new() -> Self {
max_samplers: u32,
}
impl Default for UiLimits {
fn default() -> Self {
Self { Self {
max_textures: 100000, primitives: Vec::new(),
max_samplers: 1000,
} }
} }
} }
impl UiLimits { impl ListBuffers {
pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 { fn new(device: &Device) -> Self {
self.max_textures + self.max_samplers 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::{ use crate::{
Color, UiRegion, WidgetId, Color, TextureHandle, UiData, UiRegion, WidgetId,
render::{ render::{
ArrBuf,
data::{MaskIdx, PrimitiveInstance}, data::{MaskIdx, PrimitiveInstance},
page::GlyphRender,
texture::ImageRender,
}, },
util::Vec2, util::{HashMap, Vec2},
}; };
use bytemuck::Pod; 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>, instances: Vec<PrimitiveInstance>,
/// The widget each instance belongs to, for renumbering its handles.
assoc: Vec<WidgetId>, assoc: Vec<WidgetId>,
data: PrimitiveData,
free: Vec<usize>, free: Vec<usize>,
/// `stride` bytes of the primitive's own data per instance.
data: Vec<u8>,
/// From the type the list was made for, so a write is never checked.
stride: usize,
}
impl InstanceList {
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, pub updated: bool,
} }
impl Default for Primitives { impl Default for LayerDraws {
fn default() -> Self { fn default() -> Self {
Self { Self {
instances: Default::default(), primitives: Vec::new(),
assoc: Default::default(),
data: Default::default(),
free: Vec::new(),
updated: true, updated: true,
} }
} }
} }
pub trait Primitive: Pod { impl LayerDraws {
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 {
pub fn write<P: Primitive>( pub fn write<P: Primitive>(
&mut self, &mut self,
layer: usize, layer: usize,
PrimitiveInst { PrimitiveInst {
kind,
id, id,
primitive, primitive,
region, region,
@@ -117,65 +249,67 @@ impl Primitives {
}: PrimitiveInst<P>, }: PrimitiveInst<P>,
) -> PrimitiveHandle { ) -> PrimitiveHandle {
self.updated = true; self.updated = true;
let vec = P::vec(&mut self.data); // Grown on first use rather than sized from the registry, which a
let i = vec.add(primitive); // layer cannot see.
let inst = PrimitiveInstance { if self.primitives.len() <= kind.id as usize {
region, self.primitives.resize_with(kind.id as usize + 1, || None);
idx: i as u32, }
mask_idx, let inst_idx = self.primitives[kind.id as usize]
binding: P::BINDING, .get_or_insert_with(InstanceList::new::<P>)
}; .push(
let inst_i = if let Some(i) = self.free.pop() { id,
self.instances[i] = inst; PrimitiveInstance { region, mask_idx },
self.assoc[i] = id; bytemuck::bytes_of(&primitive),
i );
} else { PrimitiveHandle {
let i = self.instances.len(); layer,
self.instances.push(inst); kind: kind.id,
self.assoc.push(id); inst_idx,
i }
}; }
PrimitiveHandle::new::<P>(layer, inst_i, i)
pub fn primitives(&self) -> &[Option<InstanceList>] {
&self.primitives
} }
/// returns (old index, new index)
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> { pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
self.free.sort_by(|a, b| b.cmp(a)); self.primitives
self.free.drain(..).filter_map(|i| { .iter_mut()
self.instances.swap_remove(i); .enumerate()
self.assoc.swap_remove(i); .filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?)))
if i == self.instances.len() { .flat_map(|(kind, list)| list.apply_free(kind))
return None;
}
let id = self.assoc[i];
let old = self.instances.len();
Some(PrimitiveChange { id, old, new: i })
})
} }
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self.updated = true; self.updated = true;
self.data.free(h.binding, h.data_idx); self.list(h).free(h.inst_idx)
self.free.push(h.inst_idx);
self.instances[h.inst_idx].mask_idx
}
pub fn data(&self) -> &PrimitiveData {
&self.data
}
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
&self.instances
} }
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion { pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
self.updated = true; self.updated = true;
&mut self.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 struct PrimitiveChange {
pub id: WidgetId, pub id: WidgetId,
/// Which registered primitive's list moved, since they index separately.
pub kind: u32,
pub old: usize, pub old: usize,
pub new: usize, pub new: usize,
} }
@@ -183,30 +317,12 @@ pub struct PrimitiveChange {
#[derive(Debug)] #[derive(Debug)]
pub struct PrimitiveHandle { pub struct PrimitiveHandle {
pub layer: usize, pub layer: usize,
pub kind: u32,
pub inst_idx: usize, pub inst_idx: usize,
pub data_idx: usize,
pub binding: u32,
} }
impl PrimitiveHandle {
fn new<P: Primitive>(layer: usize, inst_idx: usize, data_idx: usize) -> Self {
Self {
layer,
inst_idx,
data_idx,
binding: P::BINDING,
}
}
}
primitives!(
rects: RectPrimitive => 0,
textures: TexturePrimitive => 1,
glyphs: GlyphPrimitive => 2,
);
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RectPrimitive { pub struct RectPrimitive {
pub color: Color<u8>, pub color: Color<u8>,
pub radius: f32, pub radius: f32,
@@ -214,6 +330,10 @@ pub struct RectPrimitive {
pub inner_radius: f32, pub inner_radius: f32,
} }
impl Primitive for RectPrimitive {
const WGSL: &'static str = include_str!("shader/rect.wgsl");
}
impl RectPrimitive { impl RectPrimitive {
pub fn color(color: Color<u8>) -> Self { pub fn color(color: Color<u8>) -> Self {
Self { Self {
@@ -225,71 +345,51 @@ impl RectPrimitive {
} }
} }
#[repr(C)] /// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
#[derive(Debug, Copy, Clone)] /// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
pub struct TexturePrimitive { #[repr(C, align(8))]
pub view_idx: u32,
pub sampler_idx: u32,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive { pub struct GlyphPrimitive {
pub uv_min: Vec2, pub uv_min: Vec2,
pub uv_max: Vec2, pub uv_max: Vec2,
pub view_idx: u32, /// Which atlas array layer this glyph is on.
pub sampler_idx: u32, pub layer: u32,
pub color: Color<u8>, pub color: Color<u8>,
pub flags: u32, pub flags: u32,
} }
pub struct PrimitiveVec<T> { // Manual rather than derived: the align(8) leaves four bytes of padding, which
vec: Vec<T>, // is how WGSL lays the struct out.
free: Vec<usize>, 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> { /// One drawn image. Its shader reads nothing per instance; the slot names the
pub fn new() -> Self { /// texture to bind for it.
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TexturePrimitive {
pub slot: u32,
}
impl Primitive for TexturePrimitive {
const WGSL: &'static str = include_str!("shader/texture.wgsl");
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
Box::new(ImageRender::new(device, queue))
}
}
impl From<&TextureHandle> for TexturePrimitive {
fn from(handle: &TextureHandle) -> Self {
Self { Self {
vec: Vec::new(), slot: handle.slot(),
free: Vec::new(),
} }
} }
pub fn add(&mut self, t: T) -> usize {
if let Some(i) = self.free.pop() {
self.vec[i] = t;
i
} else {
let i = self.vec.len();
self.vec.push(t);
i
}
}
pub fn free(&mut self, i: usize) {
self.free.push(i);
}
pub fn clear(&mut self) {
self.free.clear();
self.vec.clear();
}
}
impl<T> Default for PrimitiveVec<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Deref for PrimitiveVec<T> {
type Target = Vec<T>;
fn deref(&self) -> &Self::Target {
&self.vec
}
}
impl<T> DerefMut for PrimitiveVec<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.vec
}
} }
-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));
}
+194 -142
View File
@@ -1,119 +1,119 @@
use image::{DynamicImage, EncodableLayout, GenericImageView}; use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage};
use wgpu::{util::DeviceExt, *}; 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 { pub struct GpuTextures {
device: Device, device: Device,
queue: Queue, queue: Queue,
/// Parallel to `views`; patches require textures rather than views. slots: Vec<Option<ImageGpu>>,
textures: Vec<Option<Texture>>, }
views: Vec<TextureView>,
view_count: usize, struct ImageGpu {
samplers: Vec<Sampler>, /// Kept for `patch`, which needs the texture rather than the view.
null_view: TextureView, texture: Texture,
no_views: Vec<TextureView>, group: BindGroup,
} }
impl GpuTextures { impl GpuTextures {
pub fn update(&mut self, textures: &mut Textures) -> bool { pub fn new(device: &Device, queue: &Queue) -> Self {
let mut bindings_changed = false; 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() { for update in textures.updates() {
bindings_changed |= match update { match update {
TextureUpdate::Push(image) => { TextureUpdate::Push(image) => {
self.push(image); let image = self.create(image, layout, sampler);
true self.slots.push(Some(image));
} }
TextureUpdate::Set(i, image) => { TextureUpdate::Set(i, image) => {
self.set(i, image); let image = self.create(image, layout, sampler);
true self.slots[i as usize] = Some(image);
} }
TextureUpdate::Patch(i, rect, image) => { TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
self.patch(i, rect, image); TextureUpdate::PushFree => self.slots.push(None),
false 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) { pub fn group(&self, slot: u32) -> Option<&BindGroup> {
let Some(texture) = &self.textures[i as usize] else { self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
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,
},
);
} }
fn create(&self, image: &DynamicImage) -> (Texture, TextureView) { fn create(
let image = image.to_rgba8(); &self,
let (width, height) = image.dimensions(); 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( let texture = self.device.create_texture_with_data(
&self.queue, &self.queue,
&TextureDescriptor { &TextureDescriptor {
label: None, label: Some("image"),
size: Extent3d { size: Extent3d {
width, width,
height, height,
@@ -127,63 +127,115 @@ impl GpuTextures {
view_formats: &[], view_formats: &[],
}, },
wgt::TextureDataOrder::MipMajor, wgt::TextureDataOrder::MipMajor,
image.as_bytes(), rgba.as_bytes(),
); );
let view = texture.create_view(&TextureViewDescriptor::default()); 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 { fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
let null_view = null_texture_view(device); let Some(Some(slot)) = self.slots.get(i as usize) else {
Self { return;
device: device.clone(), };
queue: queue.clone(), let dst = TexelCopyTextureInfo {
textures: Vec::new(), texture: &slot.texture,
views: Vec::new(), mip_level: 0,
samplers: vec![default_sampler(device)], origin: Origin3d {
no_views: vec![null_view.clone()], x: rect.x,
null_view, y: rect.y,
view_count: 0, 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
}
.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 { pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) {
device if rect.width == 0 || rect.height == 0 {
.create_texture(&TextureDescriptor { return;
label: Some("null"), }
size: Extent3d { let stride = src.width() * 4;
width: 1, queue.write_texture(
height: 1, dst,
depth_or_array_layers: 1, src.as_bytes(),
TexelCopyBufferLayout {
offset: (rect.y * stride + rect.x * 4) as u64,
bytes_per_row: Some(stride),
rows_per_image: Some(rect.height),
},
Extent3d {
width: rect.width,
height: rect.height,
depth_or_array_layers: 1,
},
);
}
/// What a primitive that samples binds: a texture, and the sampler that reads
/// it.
pub fn sampled_group(
device: &Device,
layout: &BindGroupLayout,
view: &TextureView,
sampler: &Sampler,
label: &'static str,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(view),
}, },
mip_level_count: 1, BindGroupEntry {
sample_count: 1, binding: 1,
dimension: TextureDimension::D2, resource: BindingResource::Sampler(sampler),
format: TextureFormat::Rgba8Unorm, },
usage: TextureUsages::TEXTURE_BINDING, ],
view_formats: &[], label: Some(label),
}) })
.create_view(&TextureViewDescriptor::default()) }
/// 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),
})
} }
pub fn default_sampler(device: &Device) -> Sampler { pub fn default_sampler(device: &Device) -> Sampler {
+10 -6
View File
@@ -21,17 +21,25 @@ impl<T: Pod> ArrBuf<T> {
_pd: PhantomData, _pd: PhantomData,
} }
} }
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) { /// Returns whether the `Buffer` was recreated, which stales any cached
if self.len != data.len() { /// `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.len = data.len();
self.buffer = self.buffer =
Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label); Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label);
} }
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data)); 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 { fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64; let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) { if usage.contains(BufferUsages::STORAGE) {
// A binding cannot be empty or under the layout's minimum.
size = size.max(std::mem::size_of::<T>() as u64); size = size.max(std::mem::size_of::<T>() as u64);
} }
device.create_buffer(&BufferDescriptor { device.create_buffer(&BufferDescriptor {
@@ -41,8 +49,4 @@ impl<T: Pod> ArrBuf<T> {
usage, 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 active;
mod cache; mod cache;
@@ -7,13 +9,15 @@ mod render_state;
mod size; mod size;
pub use active::*; pub use active::*;
pub use painter::Painter; pub use painter::{Painter, PrimitiveLike};
pub use render_state::*; pub use render_state::*;
pub use size::*; pub use size::*;
#[derive(Default)] #[derive(Default)]
pub struct UiData { pub struct UiData {
pub widgets: Widgets, pub widgets: Widgets,
/// Every primitive this ui can draw.
pub primitives: PrimitiveRegistry,
pub textures: Textures, pub textures: Textures,
pub text: TextData, pub text: TextData,
pub masks: TrackedArena<Mask, u32>, pub masks: TrackedArena<Mask, u32>,
+50 -22
View File
@@ -1,7 +1,10 @@
use crate::{ use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData, Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, render::{
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
},
util::Vec2, util::Vec2,
}; };
@@ -21,15 +24,26 @@ pub struct Painter<'a> {
impl<'a> Painter<'a> { impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) { fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region);
}
/// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
let h = self.state.layers.write( let h = self.state.layers.write(
self.layer, self.layer,
PrimitiveInst { PrimitiveInst {
kind,
id: self.id, id: self.id,
primitive, primitive,
region, region,
mask_idx: self.mask, mask_idx: self.mask,
}, },
); );
self.push_primitive(h);
}
fn push_primitive(&mut self, h: PrimitiveHandle) {
if self.mask != MaskIdx::NONE { if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy: // TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask); self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -38,11 +52,13 @@ impl<'a> Painter<'a> {
} }
/// Writes a primitive to be rendered /// 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) 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)); 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( pub fn render_text(
&mut self, &mut self,
buffer: &mut TextBuffer, buffer: &mut TextBuffer,
@@ -97,10 +98,12 @@ impl<'a> Painter<'a> {
width: Option<f32>, width: Option<f32>,
) -> RenderedText { ) -> RenderedText {
let ui = self.rsc.ui_mut(); let ui = self.rsc.ui_mut();
ui.text.render(buffer, attrs, width, &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) { pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() { for glyph in text.glyphs.iter() {
let mut region = origin; let mut region = origin;
region.x.end = region.x.start; region.x.end = region.x.start;
@@ -108,12 +111,12 @@ impl<'a> Painter<'a> {
let mut region = region.offset(UiVec2::abs(glyph.offset)); let mut region = region.offset(UiVec2::abs(glyph.offset));
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32); region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32); region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
self.primitive_at( self.write(
kind,
GlyphPrimitive { GlyphPrimitive {
uv_min: glyph.entry.uv_min, uv_min: glyph.entry.uv_min,
uv_max: glyph.entry.uv_max, uv_max: glyph.entry.uv_max,
view_idx: glyph.entry.view_idx, layer: glyph.entry.layer,
sampler_idx: glyph.entry.sampler_idx,
color: text.color, color: text.color,
flags: glyph.entry.flags(), flags: glyph.entry.flags(),
}, },
@@ -169,3 +172,28 @@ impl<'a> Painter<'a> {
self.state.size_ctx(self.id, self.region.size(), self.rsc) self.state.size_ctx(self.id, self.region.size(), self.rsc)
} }
} }
/// What `Painter::primitive` takes: a primitive, or something that yields one
/// and does whatever else drawing it needs.
pub trait PrimitiveLike {
type Primitive: Primitive;
fn into_primitive(self, painter: &mut Painter) -> Self::Primitive;
}
impl<P: Primitive> PrimitiveLike for P {
type Primitive = P;
fn into_primitive(self, _: &mut Painter) -> P {
self
}
}
impl PrimitiveLike for &TextureHandle {
type Primitive = TexturePrimitive;
/// Retains a share of the handle, so the slot the primitive names cannot
/// be freed and reused while it is still drawn.
fn into_primitive(self, painter: &mut Painter) -> TexturePrimitive {
painter.textures.push(self.clone());
self.into()
}
}
+14 -11
View File
@@ -1,13 +1,13 @@
use crate::{ use crate::{
ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx, ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget,
StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
ui::cache::Cache, ui::cache::Cache,
util::{HashMap, HashSet, Vec2, forget_ref}, util::{HashMap, HashSet, Vec2, forget_ref},
}; };
pub struct UiRenderState { pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>, pub active: HashMap<WidgetId, ActiveData>,
pub layers: PrimitiveLayers, pub layers: DrawLayers,
pub(super) output_size: Vec2, pub(super) output_size: Vec2,
pub cache: Cache, pub cache: Cache,
@@ -34,6 +34,10 @@ impl UiRenderState {
self.resized = true; self.resized = true;
} }
pub fn output_size(&self) -> Vec2 {
self.output_size
}
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) { pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
// safety mechanism for memory leaks; might wanna return a result instead so user can // safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not // decide whether to panic or not
@@ -243,14 +247,14 @@ impl UiRenderState {
} }
pub fn debug_layers(&self) { 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 indent = " ".repeat(depth * 2);
let len = primitives.instances().len(); let counts: Vec<String> = draws
print!("{indent}{idx}: {len} primitives"); .primitives()
if len >= 1 { .iter()
print!(" ({})", primitives.instances()[0].binding); .map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string())
} .collect();
println!(); println!("{indent}{idx}: [{}]", counts.join(", "));
} }
} }
@@ -308,7 +312,6 @@ impl UiRenderState {
source, source,
cache: &mut self.cache, cache: &mut self.cache,
text: &mut ui.text, text: &mut ui.text,
textures: &mut ui.textures,
widgets: &ui.widgets, widgets: &ui.widgets,
outer, outer,
output_size: self.output_size, output_size: self.output_size,
+3 -5
View File
@@ -1,11 +1,10 @@
use crate::{ use crate::{
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures, Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
}; };
pub struct SizeCtx<'a> { pub struct SizeCtx<'a> {
pub text: &'a mut TextData, pub text: &'a mut TextData,
pub textures: &'a mut Textures,
pub(super) source: WidgetId, pub(super) source: WidgetId,
pub(super) widgets: &'a Widgets, pub(super) widgets: &'a Widgets,
pub(super) cache: &'a mut Cache, pub(super) cache: &'a mut Cache,
@@ -33,7 +32,6 @@ impl SizeCtx<'_> {
.get_dyn_dynamic(id) .get_dyn_dynamic(id)
.desired_len::<A>(&mut SizeCtx { .desired_len::<A>(&mut SizeCtx {
text: self.text, text: self.text,
textures: self.textures,
source: self.source, source: self.source,
widgets: self.widgets, widgets: self.widgets,
cache: self.cache, cache: self.cache,
@@ -82,7 +80,7 @@ impl SizeCtx<'_> {
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, width: Option<f32>,
) -> RenderedText { ) -> RenderedText {
self.text.render(buffer, attrs, width, self.textures) self.text.render(buffer, attrs, width)
} }
pub fn label(&self, id: WidgetId) -> &String { pub fn label(&self, id: WidgetId) -> &String {
+1 -5
View File
@@ -10,11 +10,7 @@ struct State {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new( fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
rect(Color::RED).set_root(rsc, &mut ui_state); rect(Color::RED).set_root(rsc, &mut ui_state);
Self { ui_state } Self { ui_state }
} }
+3 -7
View File
@@ -15,11 +15,7 @@ pub struct Client {
} }
impl DefaultAppState for Client { impl DefaultAppState for Client {
fn new( fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let rrect = rect(Color::WHITE).radius(20); let rrect = rect(Color::WHITE).radius(20);
let pad_test = ( let pad_test = (
rrect.color(Color::BLUE), rrect.color(Color::BLUE),
@@ -212,10 +208,10 @@ impl DefaultAppState for Client {
render: &mut UiRenderState, render: &mut UiRenderState,
) { ) {
let new = format!( let new = format!(
"widgets: {}\nactive: {}\nviews: {}", "widgets: {}\nactive: {}\ntextures: {}",
rsc.widgets().len(), rsc.widgets().len(),
render.active_widgets(), render.active_widgets(),
self.ui_state.renderer.ui.view_count(), rsc.ui().textures.count(),
); );
if new != *rsc.widgets()[self.info].content { if new != *rsc.widgets()[self.info].content {
*rsc.widgets_mut()[self.info].content = new; *rsc.widgets_mut()[self.info].content = new;
+1 -5
View File
@@ -11,11 +11,7 @@ struct State {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new( fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let rect = rect(Color::RED).add(rsc); let rect = rect(Color::RED).add(rsc);
rect.task_on(CursorSense::click(), async move |mut ctx| { rect.task_on(CursorSense::click(), async move |mut ctx| {
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
+1 -5
View File
@@ -36,11 +36,7 @@ impl Test {
} }
impl DefaultAppState for State { impl DefaultAppState for State {
fn new( fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let test = Test::new(rsc); let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| { test.on(CursorSense::click(), move |_, rsc| {
+61 -47
View File
@@ -1,10 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use arboard::Clipboard; use arboard::Clipboard;
use std::{ use std::{marker::PhantomData, sync::Arc, time::Instant};
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use winit::{ use winit::{
event::{Ime, WindowEvent}, event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoopProxy},
@@ -29,7 +25,35 @@ pub use sense::*;
pub use state::*; pub use state::*;
pub use task::*; pub use task::*;
pub type Proxy<Event> = EventLoopProxy<Event>; /// Sends an application's own events to its event loop. It wraps the proxy
/// rather than being one because task updates travel the same way: what an
/// application sends is its `Event`, not the loop's whole message type.
pub struct Proxy<State: DefaultAppState>(EventLoopProxy<DefaultEvent<State>>);
impl<State: DefaultAppState> Clone for Proxy<State> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<State: DefaultAppState> Proxy<State> {
pub fn send_event(&self, event: State::Event) {
let _ = self.0.send_event(DefaultEvent::User(event));
}
}
/// What the event loop carries: the application's own events, and the
/// updates tasks send back to the ui thread.
pub enum DefaultEvent<State: DefaultAppState> {
User(State::Event),
Update(Box<dyn TaskUpdate<DefaultRsc<State>>>),
}
impl<State: DefaultAppState> TaskQueue<DefaultRsc<State>> for Proxy<State> {
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<State>>>) {
let _ = self.0.send_event(DefaultEvent::Update(update));
}
}
pub struct DefaultUiState { pub struct DefaultUiState {
pub root: Option<StrongWidget>, pub root: Option<StrongWidget>,
@@ -70,9 +94,8 @@ pub trait HasDefaultUiState: Sized + 'static {
} }
pub trait DefaultAppState: HasDefaultUiState { pub trait DefaultAppState: HasDefaultUiState {
type Event = (); type Event: Send = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>) fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self>) -> Self;
-> Self;
#[allow(unused_variables)] #[allow(unused_variables)]
fn event( fn event(
&mut self, &mut self,
@@ -105,18 +128,14 @@ pub struct DefaultRsc<State: 'static> {
} }
impl<State> DefaultRsc<State> { impl<State> DefaultRsc<State> {
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) { pub fn init(queue: Arc<dyn TaskQueue<Self>>) -> Self {
let (tasks, recv) = Tasks::init(window); Self {
( ui: Default::default(),
Self { events: Default::default(),
ui: Default::default(), tasks: Tasks::init(queue),
events: Default::default(), state: Default::default(),
tasks, _state: Default::default(),
state: Default::default(), }
_state: Default::default(),
},
recv,
)
} }
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> { pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
@@ -181,43 +200,32 @@ pub struct DefaultApp<State: DefaultAppState> {
rsc: DefaultRsc<State>, rsc: DefaultRsc<State>,
render: UiRenderState, render: UiRenderState,
state: State, state: State,
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
} }
impl<State: DefaultAppState> AppState for DefaultApp<State> { impl<State: DefaultAppState> AppState for DefaultApp<State> {
type Event = State::Event; type Event = DefaultEvent<State>;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self { fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = event_loop let window = event_loop
.create_window(State::window_attributes()) .create_window(State::window_attributes())
.unwrap(); .unwrap();
let default_state = DefaultUiState::new(window); let default_state = DefaultUiState::new(window);
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone()); let mut rsc = DefaultRsc::init(Arc::new(Proxy(proxy.clone())));
let state = State::new(default_state, &mut rsc, proxy); let state = State::new(default_state, &mut rsc, Proxy(proxy));
let render = UiRenderState::new(); let render = UiRenderState::new();
Self { Self { rsc, state, render }
rsc,
state,
render,
task_recv,
}
} }
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) { fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.state.event(event, &mut self.rsc, &mut self.render); match event {
DefaultEvent::User(event) => self.state.event(event, &mut self.rsc, &mut self.render),
DefaultEvent::Update(update) => update(&mut self.state, &mut self.rsc),
}
self.request_redraw_if_needed();
} }
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) { fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self { let Self { rsc, render, state } = self;
rsc,
render,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let ui_state = state.default_state_mut(); let ui_state = state.default_state_mut();
let input_changed = ui_state.input.event(&event); let input_changed = ui_state.input.event(&event);
@@ -297,11 +305,8 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
_ => (), _ => (),
} }
state.window_event(event, rsc, render); state.window_event(event, rsc, render);
let ui_state = self.state.default_state_mut(); self.request_redraw_if_needed();
if render.needs_redraw(&ui_state.root, rsc.widgets()) { self.state.default_state_mut().input.end_frame();
ui_state.renderer.window().request_redraw();
}
ui_state.input.end_frame();
} }
fn exit(&mut self) { fn exit(&mut self) {
@@ -309,6 +314,15 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
} }
} }
impl<State: DefaultAppState> DefaultApp<State> {
fn request_redraw_if_needed(&mut self) {
let ui_state = self.state.default_state_mut();
if self.render.needs_redraw(&ui_state.root, self.rsc.widgets()) {
ui_state.renderer.window().request_redraw();
}
}
}
pub trait RscIdx<Rsc> { pub trait RscIdx<Rsc> {
type Output; type Output;
fn get(self, rsc: &Rsc) -> &Self::Output; fn get(self, rsc: &Rsc) -> &Self::Output;
+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 pollster::FutureExt;
use std::sync::Arc; use std::sync::Arc;
use wgpu::*; use wgpu::*;
@@ -22,7 +22,20 @@ impl UiRenderer {
} }
pub fn draw(&mut self) { 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 let view = output
.texture .texture
.create_view(&TextureViewDescriptor::default()); .create_view(&TextureViewDescriptor::default());
@@ -46,7 +59,7 @@ impl UiRenderer {
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
self.window.pre_present_notify(); self.window.pre_present_notify();
output.present(); self.queue.present(output);
} }
pub fn resize(&mut self, size: &PhysicalSize<u32>) { pub fn resize(&mut self, size: &PhysicalSize<u32>) {
@@ -65,9 +78,10 @@ impl UiRenderer {
pub fn new(window: Arc<Window>) -> Self { pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size(); let size = window.inner_size();
let instance = Instance::new(&InstanceDescriptor { let instance = Instance::new(InstanceDescriptor {
backends: Backends::PRIMARY, backends: Backends::PRIMARY,
..Default::default() display: Some(Box::new(window.clone())),
..InstanceDescriptor::new_without_display_handle()
}); });
let surface = instance let surface = instance
@@ -79,22 +93,14 @@ impl UiRenderer {
power_preference: PowerPreference::default(), power_preference: PowerPreference::default(),
compatible_surface: Some(&surface), compatible_surface: Some(&surface),
force_fallback_adapter: false, force_fallback_adapter: false,
apply_limit_buckets: false,
}) })
.block_on() .block_on()
.expect("Could not get adapter!"); .expect("Could not get adapter!");
let ui_limits = UiLimits::default();
let (device, queue) = adapter let (device, queue) = adapter
.request_device(&DeviceDescriptor { .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 { 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, max_buffer_size: 1 << 30,
..Default::default() ..Default::default()
}, },
@@ -114,6 +120,7 @@ impl UiRenderer {
let config = SurfaceConfiguration { let config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT, usage: TextureUsages::RENDER_ATTACHMENT,
format: surface_format, format: surface_format,
color_space: SurfaceColorSpace::Auto,
width: size.width, width: size.width,
height: size.height, height: size.height,
present_mode: PresentMode::AutoVsync, present_mode: PresentMode::AutoVsync,
@@ -126,7 +133,7 @@ impl UiRenderer {
let encoder = Self::create_encoder(&device); let encoder = Self::create_encoder(&device);
let ui = UiRenderNode::new(&device, &queue, &config, ui_limits); let ui = UiRenderNode::new(&device, &config);
Self { Self {
surface, surface,
+16 -34
View File
@@ -1,11 +1,5 @@
use iris_core::HasState; use iris_core::HasState;
use std::{ use std::{pin::Pin, sync::Arc};
pin::Pin,
sync::{
Arc,
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
},
};
use tokio::{ use tokio::{
runtime::Runtime, runtime::Runtime,
sync::mpsc::{ sync::mpsc::{
@@ -13,64 +7,52 @@ use tokio::{
unbounded_channel as async_channel, unbounded_channel as async_channel,
}, },
}; };
use winit::window::Window;
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {} pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {} impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
/// Hands an update from a task to the thread that owns the ui. Delivery and
/// waking are one act: a host posts the update as a message its loop already
/// carries, so nothing has to wake the loop separately, or claim a redraw to
/// be looked at.
pub trait TaskQueue<Rsc: HasState>: Send + Sync + 'static {
fn send(&self, update: Box<dyn TaskUpdate<Rsc>>);
}
pub struct Tasks<Rsc: HasState> { pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>, start: AsyncSender<BoxTask>,
window: Arc<Window>, queue: Arc<dyn TaskQueue<Rsc>>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
} }
pub struct TaskCtx<Rsc: HasState> { pub struct TaskCtx<Rsc: HasState> {
send: TaskMsgSender<Rsc>, queue: Arc<dyn TaskQueue<Rsc>>,
} }
impl<Rsc: HasState> TaskCtx<Rsc> { impl<Rsc: HasState> TaskCtx<Rsc> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) { pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
let _ = self.send.send(Box::new(f)); self.queue.send(Box::new(f));
}
}
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
fn new(send: TaskMsgSender<Rsc>) -> Self {
Self { send }
} }
} }
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>; type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> { impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) { pub fn init(queue: Arc<dyn TaskQueue<Rsc>>) -> Self {
let (start, start_recv) = async_channel(); let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| { std::thread::spawn(|| {
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(listen(start_recv)) rt.block_on(listen(start_recv))
}); });
( Self { start, queue }
Self {
start,
msg_send: msgs,
window,
},
msgs_recv,
)
} }
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F) pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
where where
F::CallOnceFuture: Send, F::CallOnceFuture: Send,
{ {
let send = self.msg_send.clone(); let queue = self.queue.clone();
let window = self.window.clone();
let _ = self.start.send(Box::pin(async move { let _ = self.start.send(Box::pin(async move {
task(TaskCtx::new(send)).await; task(TaskCtx { queue }).await;
window.request_redraw();
})); }));
} }
} }
+181
View File
@@ -0,0 +1,181 @@
//! A ui with no window: build a tree, run frames, move a pointer, and read
//! back where widgets landed.
//!
//! It does not draw. A claim about pixels still needs a real surface.
use crate::prelude::*;
use std::{
sync::{
Arc,
mpsc::{Receiver, SyncSender, sync_channel},
},
time::Duration,
};
/// There is no loop here to post to, so updates queue until the test asks
/// for them.
struct Queue(SyncSender<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>);
impl TaskQueue<DefaultRsc<HarnessState>> for Queue {
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>) {
let _ = self.0.send(update);
}
}
/// `assert_eq!` for where a frame put a widget, written as its two corners.
#[macro_export]
macro_rules! assert_corners {
($harness:expr, $id:expr, ($x0:expr, $y0:expr), ($x1:expr, $y1:expr)) => {
assert_eq!(
$harness.region(&$id).expect("widget drew nothing"),
$crate::core::PixelRegion {
top_left: $crate::core::util::Vec2::new($x0 as f32, $y0 as f32),
bot_right: $crate::core::util::Vec2::new($x1 as f32, $y1 as f32),
}
);
};
}
pub use crate::assert_corners;
#[derive(Default)]
pub struct HarnessState {
pub root: Option<StrongWidget>,
}
impl HasRoot for HarnessState {
fn set_root(&mut self, root: StrongWidget) {
self.root = Some(root);
}
}
pub struct Harness {
pub rsc: DefaultRsc<HarnessState>,
pub render: UiRenderState,
pub state: HarnessState,
updates: Receiver<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>,
cursor: CursorState,
}
impl Harness {
/// `size` is the output in physical pixels.
pub fn new(size: impl Into<Vec2>) -> Self {
// A `TaskQueue` must be `Sync`, which `mpsc::Sender` is not; the
// bound that comes with `SyncSender` is far past anything a test
// leaves unread.
let (send, updates) = sync_channel(1024);
let rsc = DefaultRsc::init(Arc::new(Queue(send)));
let mut render = UiRenderState::new();
render.resize(size);
Self {
rsc,
render,
state: HarnessState::default(),
updates,
cursor: CursorState::default(),
}
}
pub fn size(&self) -> Vec2 {
self.render.output_size()
}
pub fn resize(&mut self, size: impl Into<Vec2>) {
self.render.resize(size);
}
/// Sets the root and lays it out, so a pointer event has something to hit.
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
widget.set_root(&mut self.rsc, &mut self.state);
self.frame();
}
pub fn needs_redraw(&self) -> bool {
self.render
.needs_redraw(&self.state.root, self.rsc.widgets())
}
pub fn apply_updates(&mut self) -> usize {
let mut applied = 0;
while let Ok(update) = self.updates.try_recv() {
update(&mut self.state, &mut self.rsc);
applied += 1;
}
applied
}
/// Waits for a task's first update, then applies everything waiting.
/// False if none arrived in time.
#[must_use]
pub fn await_update(&mut self, timeout: Duration) -> bool {
let Ok(update) = self.updates.recv_timeout(timeout) else {
return false;
};
update(&mut self.state, &mut self.rsc);
self.apply_updates();
true
}
/// Lays the tree out and builds its primitives.
pub fn frame(&mut self) {
self.apply_updates();
self.render.update(&self.state.root, &mut self.rsc);
}
/// Where the last frame put a widget, or `None` if it drew nothing.
pub fn region(&self, id: &impl IdLike) -> Option<PixelRegion> {
self.render.window_region(id)
}
pub fn move_to(&mut self, pos: impl Into<Vec2>) {
self.cursor.pos = pos.into();
self.cursor.exists = true;
self.sense();
}
pub fn leave(&mut self) {
self.cursor.exists = false;
self.sense();
}
pub fn press(&mut self, button: CursorButton) {
self.button(button).update(true);
self.sense();
}
pub fn release(&mut self, button: CursorButton) {
self.button(button).update(false);
self.sense();
}
/// A wheel carries no position, so this goes wherever the cursor was last
/// moved to -- nowhere, until it has been moved.
pub fn scroll(&mut self, delta: impl Into<Vec2>) {
self.cursor.scroll_delta = delta.into();
self.sense();
}
pub fn click(&mut self, pos: impl Into<Vec2>) {
self.move_to(pos);
self.press(CursorButton::Left);
self.release(CursorButton::Left);
}
fn button(&mut self, button: CursorButton) -> &mut ActivationState {
let buttons = &mut self.cursor.buttons;
match button {
CursorButton::Left => &mut buttons.left,
CursorButton::Middle => &mut buttons.middle,
CursorButton::Right => &mut buttons.right,
}
}
/// Dispatches against the layout of the last frame, which is what a
/// window delivers input against too.
fn sense(&mut self) {
let cursor = self.cursor.clone();
let size = self.render.output_size();
self.render
.run_sensors(&mut self.rsc, &mut self.state, cursor, size);
self.cursor.end_frame();
}
}
+1
View File
@@ -7,6 +7,7 @@
pub mod default; pub mod default;
pub mod event; pub mod event;
pub mod harness;
pub mod widget; pub mod widget;
pub use iris_core as core; pub use iris_core as core;
+1 -1
View File
@@ -7,7 +7,7 @@ pub struct Image {
impl Widget for Image { impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
painter.texture(&self.handle); painter.primitive(&self.handle);
} }
fn desired_width(&mut self, _: &mut SizeCtx) -> Len { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
+2 -2
View File
@@ -4,7 +4,7 @@ mod max_size;
mod offset; mod offset;
mod pad; mod pad;
mod scroll; mod scroll;
mod sized; mod set_size;
mod span; mod span;
mod stack; mod stack;
@@ -14,6 +14,6 @@ pub use max_size::*;
pub use offset::*; pub use offset::*;
pub use pad::*; pub use pad::*;
pub use scroll::*; pub use scroll::*;
pub use sized::*; pub use set_size::*;
pub use span::*; pub use span::*;
pub use stack::*; pub use stack::*;
@@ -1,12 +1,12 @@
use crate::prelude::*; use crate::prelude::*;
pub struct Sized { pub struct SetSize {
pub inner: StrongWidget, pub inner: StrongWidget,
pub x: Option<Len>, pub x: Option<Len>,
pub y: Option<Len>, pub y: Option<Len>,
} }
impl Sized { impl SetSize {
fn apply_to_outer(&self, ctx: &mut SizeCtx) { fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x { if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest()); ctx.outer.x.select_len(x.apply_rest());
@@ -17,7 +17,7 @@ impl Sized {
} }
} }
impl Widget for Sized { impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner); painter.widget(&self.inner);
} }
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::{Sized, Unsize}; use std::marker::Unsize;
pub struct WidgetPtr { pub struct WidgetPtr {
pub inner: Option<StrongWidget>, pub inner: Option<StrongWidget>,
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::{PhantomData, Sized}; use std::marker::PhantomData;
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> { pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
pub content: String, pub content: String,
+6 -6
View File
@@ -31,9 +31,9 @@ widget_trait! {
} }
} }
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> { fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> {
let size = size.into(); let size = size.into();
move |state| Sized { move |state| SetSize {
inner: self.add_strong(state), inner: self.add_strong(state),
x: Some(size.x), x: Some(size.x),
y: Some(size.y), y: Some(size.y),
@@ -58,18 +58,18 @@ widget_trait! {
} }
} }
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> { fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into(); let len = len.into();
move |state| Sized { move |state| SetSize {
inner: self.add_strong(state), inner: self.add_strong(state),
x: Some(len), x: Some(len),
y: None, y: None,
} }
} }
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> { fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into(); let len = len.into();
move |state| Sized { move |state| SetSize {
inner: self.add_strong(state), inner: self.add_strong(state),
x: None, x: None,
y: Some(len), 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
);
}
}
+34
View File
@@ -0,0 +1,34 @@
//! Where a frame puts things, with no window to put them in.
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
let left = rect(Color::RED).width(100).add(&mut h.rsc);
let right = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
(left.id(), right.id())
}
#[test]
fn a_span_gives_each_child_the_width_it_asked_for() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
assert_corners!(h, left, (0, 0), (100, 200));
assert_corners!(h, right, (100, 0), (400, 200));
}
#[test]
fn resizing_relays_out_against_the_new_output() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_corners!(h, left, (0, 0), (100, 100));
assert_corners!(h, right, (100, 0), (800, 100));
}
+60
View File
@@ -0,0 +1,60 @@
//! Which widget an input reaches.
use std::{cell::RefCell, rc::Rc};
use iris::harness::Harness;
use iris::prelude::*;
#[test]
fn a_press_reaches_only_the_widget_under_the_cursor() {
let mut h = Harness::new((400, 200));
let clicks = Rc::new(RefCell::new(Vec::new()));
let (on_left, on_right) = (clicks.clone(), clicks.clone());
let left = rect(Color::RED)
.width(100)
.on(CursorSense::click(), move |_, _| {
on_left.borrow_mut().push("left")
})
.add(&mut h.rsc);
let right = rect(Color::BLUE)
.on(CursorSense::click(), move |_, _| {
on_right.borrow_mut().push("right")
})
.add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
h.click((50, 100));
assert_eq!(*clicks.borrow(), ["left"]);
h.click((300, 100));
assert_eq!(*clicks.borrow(), ["left", "right"]);
}
#[test]
fn hover_ends_when_the_cursor_leaves_the_window() {
let mut h = Harness::new((400, 200));
let hovered = Rc::new(RefCell::new(0));
let ended = Rc::new(RefCell::new(0));
let (h_count, e_count) = (hovered.clone(), ended.clone());
let widget = rect(Color::RED)
.on(CursorSense::HoverStart, move |_, _| {
*h_count.borrow_mut() += 1
})
.on(CursorSense::HoverEnd, move |_, _| {
*e_count.borrow_mut() += 1
})
.add(&mut h.rsc);
h.set_root(widget);
h.move_to((200, 100));
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
// A second sample inside the same widget is not a second hover.
h.move_to((210, 100));
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
h.leave();
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1));
}
+26
View File
@@ -0,0 +1,26 @@
//! Scrolling moves content and stops at its ends.
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
#[test]
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
let mut h = Harness::new((400, 200));
// Twice the window's height, so there is 200 to scroll.
let top = rect(Color::RED).height(200).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
h.move_to((200, 100));
// `Scroll` starts snapped to the end.
assert_corners!(h, top, (0, -200), (400, 0));
// The handler scales a wheel line by 50.
h.scroll((0, 1));
h.frame();
assert_corners!(h, top, (0, -150), (400, 50));
h.scroll((0, 10));
h.frame();
assert_corners!(h, top, (0, 0), (400, 200));
}
+23
View File
@@ -0,0 +1,23 @@
//! What a background task can change, and how it gets back to the ui.
use std::time::Duration;
use iris::harness::Harness;
use iris::prelude::*;
#[test]
fn a_task_update_reaches_the_tree() {
let mut h = Harness::new((400, 200));
let widget = rect(Color::RED).add(&mut h.rsc);
h.set_root(widget.task_on(CursorSense::click(), async move |mut ctx| {
ctx.update(move |_, rsc| widget(rsc).color = Color::BLUE);
}));
h.click((200, 100));
assert!(
h.await_update(Duration::from_secs(5)),
"the task sent no update"
);
assert_eq!(h.rsc[widget].color, Color::BLUE);
}