Commit Graph
3 Commits
Author SHA1 Message Date
iris-ai f9ef7514e7 Resolve a primitive's position through a chain of move slots
The plumbing for O(1) subtree movement (LAYOUT.md §2), with every slot
still at zero, so this changes no pixels and the next commit can change
behaviour against a known-good picture.

Every active widget owns a slot in `UiData::moves`: a translation in
physical pixels and the slot it is relative to. A primitive instance and
a mask each name one, and `prelude.wgsl` walks the chain and adds the
accumulated delta. A mask resolves its own chain rather than the drawn
primitive's, so a stationary viewport can clip content that moves inside
it. `CHAIN_LIMIT` is stated on both sides; it bounds a malformed cycle
rather than any real tree.

A slot outlives any one `ActiveData`, because a redraw replaces that
while the widget's children go on pointing at the slot, so it lives in
`UiRenderState::moves` keyed by widget and is retired when the widget
stops being drawn. `MoveIdx` is its own type rather than another
`Id<u32>`: it sits beside `MaskIdx` in an instance and the two must not
be swappable.

`Vec2` is now `repr(align(8))`, which is WGSL's alignment for a
`vec2<f32>`, so a GPU struct holding one is laid out the way its shader
reads it without saying so itself -- `GlyphPrimitive` no longer states
its own alignment, and `MoveOffset` never has to. Both keep a manual
`unsafe impl Pod`, since the trailing padding that alignment introduces
is what `derive(Pod)` refuses. `WindowUniform` holds the `Vec2` its
shader has always called `dim` rather than two loose floats, which was
the last place the two sides described the same bytes differently.

Checked: fmt, clippy and 40 tests. `tabs` (with the image replay),
`view` and `minimal` render byte-identical to `upstream/main`, and
`text` is unchanged.
2026-09-14 03:05:14 -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