The `primitives!` macro, `PrimitiveData`, `PrimitiveVec`,
`PrimitiveBuffers`, the `Primitive` trait and the shader's dispatch
switch are gone. A primitive is now a registration: its WGSL and,
implicitly, the size of the entry that WGSL reads. Everything else --
its instance list, its free list, its buffers, its bind group and its
pipeline -- follows from that, so adding one is a `register` call and a
shader file, with nothing per-type to remember and no cross-type
dispatch to extend.
Nothing dispatches dynamically. Push, free, renumber, upload and draw
are identical for every primitive; what differs is the entry size and
the pipeline, which are data. So `InstanceList` carries a runtime
stride and its instances' data as bytes, and one concrete type serves
every primitive and the textures. Measured against a typed list it
costs 0.2ns per write, where a trait object costs 1.6ns.
Because each type has its own list, an instance's index is also its
data index: `@builtin(instance_index)` replaces the `idx` field, the
`binding` field goes with the switch, and `PrimitiveInstance` drops from
28 bytes to 20. `PrimitiveVec`'s free list merges into the instance
list's, so an instance and its data are freed by one `swap_remove`
rather than two arenas kept in step.
`shader.wgsl` becomes `shader/prelude.wgsl` plus one file per primitive.
The prelude carries the window, masks, sampled texture, vertex shader
and `masked()`, and is compiled ahead of each primitive's own source --
which is also what a caller's own primitive would be. Masks move back
into group 0 beside the window uniform, since every pipeline shares one
layout.
Within a layer, types now draw in registration order: a rect under a
glyph under a texture. Order within a layer was never meaningful --
freeing an instance swaps another into its place -- so this replaces an
accident with a defined order, and backgrounds land under their content.
Verified with a headless run per case: text over its own rect and an
image over its own rect in one layer, a masked stack clipping, the
text-layout tab, and adding three images and deleting one.
Interleaving textures with primitives was solving a problem that does not
exist: within a layer, order is already undefined because freeing an
instance swap-removes it, and layering is what layers are for. So the run
batching is gone.
A layer is now `LayerDraws`: a `Primitives` and a texture `InstanceList`
side by side, with `updated` covering both. `Primitives` holds only
primitives again -- its instance list plus the group-1 data those
instances read -- and `InstanceList` is the shared push/free/apply_free
the two lists have in common rather than a second copy of it.
`PrimitiveHandle` names which list with `InstanceKind`, and
`PrimitiveChange` carries the same, since the two index independently.
The handle no longer carries a group-1 index at all: a primitive
instance already records where its entry is.
The renderer gives each layer a second instance buffer and draws its
textures one at a time after the instanced draw, each binding its own
group 2.
Review fixes alongside: `GlyphAtlas::allocate` returns the `PageUpload`
it reserved instead of a bare tuple; a page or image region uploads
through a new `write_region`, which passes the row stride to
`write_texture` rather than copying the rectangle out first.
Verified by replaying taps into the `tabs` example: three images added
and one deleted leaves two drawn with two live texture slots, and the
masked text-edit tab still clips, with the images freed on tab switch.
Rework of the review on #11. Pages and standalone images were one
`Textures` manager separated by a `TextureKind` tag, and images were a
second instance list beside `Primitives::instances`. The tag forced
`image_index()`/`layer()` to panic on the wrong kind of handle, and the
second list forced an `is_image` branch through `free`, `region_mut`,
`apply_free` and `PrimitiveChange`.
Pages are now their own thing. `GlyphAtlas` owns its page images
outright and hands the renderer dirty rectangles; `GpuPages` owns the
array texture they upload to. `Textures` is standalone images only, so
`TextureHandle` has one kind, `slot()` cannot be wrong, and nothing
needs a free list that skips pages. `GlyphAtlas::insert` no longer takes
a `Textures`, which drops that parameter from `TextData::render` and
`SizeCtx` too.
Images go back through the one instance list. A texture instance is an
ordinary `PrimitiveInstance` whose `idx` names a texture rather than a
group-1 entry, which `PrimitiveHandle::data_idx: Option` records.
`RenderLayer::plan_draws` batches the layer's instances into runs
sharing a bind group, so a ui with no images still plans a single draw,
and an image draws in instance order rather than on top of its layer.
Group 2 is now one `texture_2d_array` and a sampler, bound per run: the
atlas for rects and glyphs, or one image viewed as an array of one. That
removes the second texture binding and the 1x1 null view that had to
fill it. Masks move to group 3, so resizing that buffer no longer stales
every texture bind group, and `GpuTextures` no longer reports whether
the caller must rebuild one.
`GlyphPrimitive` drops its manual pad: the WGSL struct now declares the
uvs as scalars, which matches the Rust layout exactly. `#[repr(C,
align(8))]` would have left real padding bytes, which `bytemuck::Pod`
forbids.
Verified with a headless run of the `tabs` example and of a scratch
example mixing images, rects, glyphs and a mask in one layer; 52 glyphs
at size 300 grew the atlas array from 1 to 4 layers with every earlier
page still sampling correctly.
The renderer bound every texture through one
`binding_array<texture_2d<f32>>` indexed per primitive. That needs
`VK_EXT_descriptor_indexing`, which a real share of Android GPUs do not
have, so the shape did not run there at all.
Split the two things being bound, since they want opposite treatment:
- Glyph atlas pages become layers of one `texture_2d_array`. A glyph
primitive carries a layer rather than a view/sampler index pair, and a
layer index is an ordinary sampling operand -- no extension. Growing the
atlas recreates the array with headroom and copies the old layers across
GPU-side.
- A standalone image gets its own texture and its own bind group, and
draws in its own call. It no longer needs a per-instance entry in
`PrimitiveData` at all: the bind group has already picked the texture.
`Primitives` therefore keeps images in a list of their own, with
`PrimitiveChange::is_image` saying which list a renumbering belongs to --
the two have independent index spaces, so `(layer, inst_idx)` alone would
collide between them.
Verified on this machine's real GPU (Venus onto an RX 7900 XT, confirmed
by the loaded ICD rather than assumed): the `tabs` example renders
byte-identical screenshots before and after, both for a text-and-rect tab
and for one holding a standalone image.
Replace the cosmic-text path with Parley layout and Swash rasterization, backed by shared glyph-atlas pages. Shaping, editing, rasterization, and glyph rendering move together because they share the text buffer and rendered-glyph types; splitting them further would require a temporary renderer that is immediately removed.
This is reconstructed rather than replayed from the extraction history. It also fixes issues found during review:
- texture binding changes remain set when an atlas patch follows a new page
- pressing an empty field places a caret and accepts input
- selection motion delegates collapse behavior to Parley
- character deletion follows logical clusters rather than visual neighbors
- the unused root-level Swash dependency is omitted
Four public-behavior integration tests live in `tests/text_edit.rs`: empty-field input, multibyte IME preedit replacement, UTF-8-safe backspace, and selection replacement. The old twelve-test inline block and implementation-restating cases are omitted.
Every added source comment was manually reviewed. Comments that narrated implementation or history were removed; retained comments document cache/rasterization keys, GPU upload constraints, focus representation, bidi geometry, or IME semantics.
Known limitation: atlas pages currently grow without eviction. Each page is 4 MiB on CPU and GPU. An arbitrary cap would leave cached rendered-text UVs pointing at reused glyph slots, so bounding this safely needs a later generation/invalidation change.
This changes public text types and signatures. GPU glyph rendering is covered by compilation rather than a live-surface test.
Verified with:
- `cargo fmt --all --check`
- `cargo clippy --workspace --all-targets -- -D warnings`
- `cargo test --workspace` (four integration tests pass)
Cargo still reports inherited future-incompatibility notices for existing wgpu/winit dependencies; there are no current clippy warnings.
---------
Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: iris/iris#10
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>