TEXTURES.md: review -- wgpu-hal gate located, a Patch-cancels-Push bug, and a sort-free shape
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e49ce8c92
commit
4400966928
1 file changed
+120
+120
@@ -213,3 +213,123 @@ Open questions a reviewer should weigh in on:
|
|||||||
texture set should already be bounded by what is on screen rather than
|
texture set should already be bounded by what is on screen rather than
|
||||||
by the whole conversation — worth confirming that invariant holds before
|
by the whole conversation — worth confirming that invariant holds before
|
||||||
relying on it to keep atlas/bind-group churn small.
|
relying on it to keep atlas/bind-group churn small.
|
||||||
|
|
||||||
|
## Review, 2026-09-04
|
||||||
|
|
||||||
|
A second pass over the file above against the code, done before anything
|
||||||
|
is implemented. Iris's worry going in: a bind group per texture means a
|
||||||
|
draw call per image, and she wants this as efficient as it can be.
|
||||||
|
|
||||||
|
### What checked out
|
||||||
|
|
||||||
|
Every code reference above is accurate as of this commit: the 100,000 /
|
||||||
|
1,000 limits, the one-time pipeline, the `rsc_group` rebuild on every
|
||||||
|
`Push`/`Set`/`Free`, and the `Patch` exclusion. The device request that
|
||||||
|
asks for the three features is `iris/src/default/render.rs:96`, which the
|
||||||
|
text above does not name. egui-wgpu and Vello are described correctly.
|
||||||
|
|
||||||
|
### The emulator refusal is a wgpu-hal gap, now located
|
||||||
|
|
||||||
|
The file guessed "a likely instance-version negotiation gap." It is
|
||||||
|
narrower than that and it is in wgpu-hal, not the emulator. wgpu-hal
|
||||||
|
28.0.0 (`src/vulkan/adapter.rs:1618`) only queries
|
||||||
|
`PhysicalDeviceDescriptorIndexingFeaturesEXT` **when the device advertises
|
||||||
|
the `VK_EXT_descriptor_indexing` extension string**. A Vulkan 1.2+ driver
|
||||||
|
that has descriptor indexing as core need not list the extension, and
|
||||||
|
lavapipe at 1.3 evidently does not, so wgpu never asks and reports the
|
||||||
|
features absent, which is why `ash` sees seven `true`s and wgpu sees none.
|
||||||
|
The properties query beside it (line 1486) correctly accepts
|
||||||
|
`device_api_version >= 1.2 || extension`; the features query does not.
|
||||||
|
wgpu-hal 30.0.1 in the local registry has the same asymmetry (lines
|
||||||
|
1872 and 2036). Worth an upstream issue, but not a reason to keep the
|
||||||
|
design: on real phones the gate that matters is stricter still.
|
||||||
|
|
||||||
|
**wgpu's `TEXTURE_BINDING_ARRAY` needs six sub-features, not one**
|
||||||
|
(`adapter.rs:160-177`): non-uniform indexing *and* update-after-bind for
|
||||||
|
sampled images, storage images and storage buffers, all together, because
|
||||||
|
wgpu marks every array-bearing descriptor set update-after-bind. So Arm's
|
||||||
|
"the extension is supported on Valhall" is necessary but not sufficient;
|
||||||
|
a driver with sampled-image indexing and without storage-buffer
|
||||||
|
update-after-bind is refused too. That widens the excluded set beyond
|
||||||
|
what the Arm quote suggests and strengthens the conclusion.
|
||||||
|
|
||||||
|
### A live bug in the current code, found on the way
|
||||||
|
|
||||||
|
`GpuTextures::update` (`core/src/render/texture.rs:33`) implements
|
||||||
|
"a patch must not report changed" as `changed = false`, unconditionally,
|
||||||
|
which also **cancels a `Push` earlier in the same batch**. That ordering is
|
||||||
|
exactly what opening a new atlas page produces: `GlyphAtlas::allocate`
|
||||||
|
pushes the page and `insert` patches it in the same frame, so the bind
|
||||||
|
group is not rebuilt and the new page's view is not bound until some
|
||||||
|
unrelated texture change happens to rebuild it. It is hidden today only
|
||||||
|
because the masks path also sets `changed`. The fix is one line
|
||||||
|
(`changed |= !matches!(update, Patch)` in spirit); it should go in with
|
||||||
|
the redesign since that code is being replaced, and it is recorded here
|
||||||
|
so it is not rediscovered.
|
||||||
|
|
||||||
|
### In-layer draw order is already undefined
|
||||||
|
|
||||||
|
Relevant to any batching redesign: `Primitives::apply_free`
|
||||||
|
(`core/src/render/primitive.rs:147`) uses `swap_remove`, so the instance
|
||||||
|
order within a layer is permuted whenever anything is freed. Overlap order
|
||||||
|
inside one layer is therefore not something the renderer promises today;
|
||||||
|
ordering is done with layers. That means grouping a layer's draws by
|
||||||
|
texture, or drawing a layer's images after its rects and glyphs, loses
|
||||||
|
nothing that currently exists. It should be written down as an invariant
|
||||||
|
when the redesign lands, because the new code will depend on it.
|
||||||
|
|
||||||
|
### On "a draw call per image"
|
||||||
|
|
||||||
|
Two corrections to the worry. First, it is a draw per *distinct texture per
|
||||||
|
layer*, not per image primitive: every glyph quad in a layer shares the
|
||||||
|
atlas and stays one instanced draw, and a thumbnail atlas would do the same
|
||||||
|
for previews. Second, the count is bounded by what is on screen, which I3's
|
||||||
|
virtualised transcript already bounds, and a mobile GPU is not draw-call
|
||||||
|
bound at tens of draws per frame; egui ships exactly this on Android. What
|
||||||
|
does cost is per-frame *bind group creation* and per-frame *sorting*, and
|
||||||
|
the current code already creates a `primitive_group` bind group every time
|
||||||
|
a layer updates (`render/mod.rs:103`), so one more per new image is not a
|
||||||
|
regression in kind.
|
||||||
|
|
||||||
|
### Recommended shape (proposal, for Iris to accept or change)
|
||||||
|
|
||||||
|
Aimed at the fewest moving parts that need no feature beyond Vulkan 1.0:
|
||||||
|
|
||||||
|
1. **Atlas pages become layers of one `texture_2d_array`**, not separate
|
||||||
|
textures. Every page is already `PAGE`x`PAGE` RGBA8, which is the one
|
||||||
|
constraint an array texture imposes. A layer index is an ordinary
|
||||||
|
sampling operand in WGSL and needs no indexing feature, so `GLYPH`
|
||||||
|
(and any future atlased-image primitive) carries a layer instead of a
|
||||||
|
`view_idx` and all of a layer's text stays **one draw**. This answers
|
||||||
|
the open question above about keeping a small binding array: no. Cost
|
||||||
|
of opening a page: recreate the array with one more layer and
|
||||||
|
`copy_texture_to_texture` the old ones, GPU-side, no readback; grow
|
||||||
|
with headroom (double) so it is rare. wgpu's default
|
||||||
|
`max_texture_array_layers` is 256, at 4 MB each, so the cap is memory
|
||||||
|
rather than the API.
|
||||||
|
2. **Every standalone image is its own texture with its own bind group**,
|
||||||
|
and its instances live in a **separate per-layer instance list**, not
|
||||||
|
the main one. Then the main instance buffer never contains an image,
|
||||||
|
there is nothing to sort, no handle remapping beyond what
|
||||||
|
`apply_free` already does, and each image is `draw(0..4, k..k+1)` with
|
||||||
|
its bind group set first. Group 2's layout becomes `{atlas array,
|
||||||
|
one image texture, sampler, masks}`; the main draw binds a 1x1 null
|
||||||
|
image in the image slot, each image draw binds its own. One pipeline,
|
||||||
|
one shader, one layout.
|
||||||
|
3. **No thumbnail atlas in the first version.** With images on their own
|
||||||
|
textures, the threshold and eviction questions above disappear: an
|
||||||
|
image is freed when the row that owns its `TextureHandle` scrolls out.
|
||||||
|
Add an image atlas only if a measured screen shows enough small images
|
||||||
|
to matter, which a transcript rarely does.
|
||||||
|
4. **Drop the three features and the two `max_binding_array_*` limits from
|
||||||
|
`src/default/render.rs`**, and the `UiLimits` counts with them.
|
||||||
|
5. **Sampling is `NonFiltering` today** (`render/mod.rs:290,299`), so a
|
||||||
|
downscaled attachment will alias. Either request a filtering sampler
|
||||||
|
for the image slot or downscale on the CPU before upload; decide when
|
||||||
|
the image widget is touched, not as part of this.
|
||||||
|
|
||||||
|
What this costs against the file's original recommendation: `Textures`
|
||||||
|
needs to know an image from a page (two kinds of handle, or a kind on
|
||||||
|
`TextureHandle`), and `Primitives` gets a second instance list per layer.
|
||||||
|
What it saves: the sort, the size threshold, the eviction policy, and any
|
||||||
|
per-page bind group switch.
|
||||||
Reference in new issue
Block a user