iris: a one-layer glyph atlas is a GL_TEXTURE_2D, so every glyph drew as a box

The emulator was blamed for two days for what is iris's own defect on any
GL adapter. `GpuTextures::new` created the atlas `texture_2d_array` with
one layer; wgpu-hal picks the GL target from the descriptor alone
(`gles::Texture::get_info_from_desc`, `(false, 1) => TEXTURE_2D`), so the
shader's `sampler2DArray` was handed a `GL_TEXTURE_2D`, the unit was
incomplete, every `textureSample` returned (0,0,0,1), and `draw_glyph`'s
`color.a *= texel.a` painted the whole glyph quad.

`MIN_ARRAY_LAYERS = 2`, with the account at `create_array_texture` and a
`debug_assert!` there. Vulkan -- the phone's build and the desktop's
default backend -- was never affected.

`force-gles` now switches the desktop backend too, so the GLES path is
reproducible on a machine with a real GPU in seconds rather than only
through an APK: that is how this was found, with two shader probes
showing the sample was exactly (0,0,0,1).
This commit is contained in:
iris committed 2026-09-06 19:41:40 -04:00
1 parent 69525bd131
commit 3cb18ac5c2
5 files changed
+108 -14

No files matched your search

+14 -11
View File
@@ -609,17 +609,20 @@ agent ticks it here with the evidence.
format rather than the palette -- but it makes the desktop build
useless as a colour reference, which is exactly what P1a needed it for
when the emulator could not draw glyphs.
- [ ] **The emulator cannot draw iris's glyphs.** Under `-gpu host` with
Vulkan disabled (Mesa 26.2.2 / virgl -- what `emu` does on this
machine) every character renders as a solid filled box: the atlas
sample's alpha reads 1, which is what an incomplete GL texture returns
(0,0,0,1). Not new (`20303e0` does it too) and not the platform's
(Compose draws text perfectly on the same AVD in the same minute).
Enabling host Vulkan still dies at boot in gfxstream, and
`EMU_GPU=software` gives SwiftShader Vulkan on which iris **SIGSEGVs
in `surface_changed`**. Either of the last two would restore
appearance testing on Android; today it has to be done on the desktop
backend or on Iris's phone.
- [x] **Every glyph was a solid box on the GLES backend -- iris's bug,
not the emulator's.** Fixed 2026-09-06. The atlas is one
`texture_2d_array` and `GpuTextures::new` created it with **one
layer**; wgpu-hal picks the GL target from the descriptor
(`(false, 1) => TEXTURE_2D`), so under GLES that array was a
`GL_TEXTURE_2D` bound to the shader's `sampler2DArray`, the unit was
incomplete, every `textureSample` returned (0,0,0,1), and
`draw_glyph`'s `color.a *= texel.a` filled the quad. `MIN_ARRAY_LAYERS
= 2` in `iris/core/src/render/texture.rs`, with a `debug_assert!` at
`create_array_texture`. Vulkan (the phone, the desktop's default
backend) was never affected. Reproduce the class in seconds without an
emulator: `iris`'s `force-gles` feature now switches the **desktop**
backend too -- `./run-headless.sh transcript --shot /tmp/x.png -- -p
transcript-ui --features iris/force-gles`.
## Build (for the port)
+55
View File
@@ -5600,6 +5600,61 @@ device.
transcript-ui -p desktop-app -p tabs-ui --all-targets`
warning-free; `cargo test` 85 (iris, +4) + 13 (iris-core) +
31 (transcript-ui, +11) + 123 (client-core).
**2026-09-06, after P1a: "the emulator cannot draw iris's glyphs"
was iris's bug, not the emulator's.** The finding recorded in the
box above -- that every glyph is a solid filled box under `-gpu
host` GLES and that this is what an incomplete GL texture returns
-- had the mechanism right and the attribution wrong. It is a real
defect on **any** adapter that is GL rather than Vulkan.
- **Reproduced off the emulator entirely**, which is what made it
cheap: `default/render.rs` now honours the same `force-gles`
feature `android/render.rs` did, so
`./run-headless.sh transcript --shot /tmp/x.png -- -p
transcript-ui --features iris/force-gles` draws the boxes on this
machine's own GPU in seconds. Two shader probes then said what
the sample was: `return vec4(texel.rgb, 1.0)` drew black boxes and
`return vec4(texel.a, texel.a, texel.a, 1.0)` drew white ones, so
the atlas sample was exactly (0, 0, 0, 1) -- GL's answer for an
**incomplete texture unit**, and not the null texture (which is
zeroed, alpha 0).
- **Root cause: the glyph atlas array was created with one layer.**
`GpuTextures::new` started `array_capacity` at 1 and `grow_array`
only doubles once a page needs a layer past it, so the ordinary
case -- one atlas page -- is a one-layer array. wgpu-hal picks the
GL texture target from the descriptor alone
(`gles::Texture::get_info_from_desc`: `(false, 1) => TEXTURE_2D`),
so that array is created as a `GL_TEXTURE_2D` and then bound to
the shader's `sampler2DArray`. wgpu has a name for this
(`log_failing_target_heuristics`, its issues #1614/#1574); the
result is an incomplete unit, `texel.a == 1`, and `draw_glyph`'s
`color.a *= texel.a` paints the whole glyph quad.
- **Fix**: `MIN_ARRAY_LAYERS = 2` in
`iris/core/src/render/texture.rs` -- the array is never created
with fewer, with the account at `create_array_texture` and a
`debug_assert!` there so a future capacity arithmetic change fails
at the mistake rather than as boxes on a screen. Cost: one page of
texture memory, which the next atlas page uses anyway.
- **Not a regression from `3e72a4e..20303e0`, and the bisect was not
run.** The defect is a function of the layer count, not of any
commit in that range: it has been there since the atlas became a
`texture_2d_array` (TEXTURES.md, 2026-09-04) and it reproduces at
HEAD and disappears at HEAD with the one-line capacity change. The
claimed "visible text at `3e72a4e`" is a misreading of its own
evidence -- `/tmp/final-typing.png`, the screenshot that entry
cites, is boxes; what the agent read was the `iris text render:
chars=5 glyphs=5` log line, which reports what **parley shaped**,
not what reached the screen. The earlier genuinely-good emulator
shots (`/tmp/after3.png`, 2026-09-05 19:56) predate the APK being
built with `force-gles` at all, so they were the Vulkan path.
- **The phone build is not affected and does not need withdrawing.**
`android-app/build-apk.sh`'s default features are deliberately
without `force-gles` (`d73db97`'s comment), so a phone build takes
`Backends::PRIMARY` -> Vulkan, where a one-layer array is an
ordinary one-layer array and glyphs draw correctly -- which is
also what Iris's phone reports have shown all along. The fix
matters for any device that falls back to GLES, which is why it is
not just an emulator convenience.
- [ ] **P1b — tool-call cards and grouping.** `ToolRows.kt`/
`ToolInput.kt`'s cards: a collapsed row per call with name
and a one-line summary, expand to input and output, runs of
+5 -1
View File
@@ -66,7 +66,11 @@ send_wrapper = "0.6.0"
# default) or virgl's GLES path, without a second env-var plumbing path that
# nothing on this machine can hand to an already-launched Android process
# (there is no `am start` environment and no system-property reader here to
# add one). Android-only; `android/render.rs` is the only reader.
# add one). Read by `android/render.rs` and, so the GLES path can be
# reproduced on a machine with a real GPU rather than only in the emulator,
# by `default/render.rs`:
# ./run-headless.sh transcript --shot /tmp/x.png -- -p transcript-ui \
# --features iris/force-gles
force-gles = []
[dev-dependencies]
+24 -1
View File
@@ -5,6 +5,10 @@ use crate::{PatchRect, TextureKind, TextureUpdate, Textures};
use super::atlas::PAGE;
/// The fewest layers the glyph atlas array is ever created with. Two, not
/// one, for the GLES reason written on `create_array_texture`.
const MIN_ARRAY_LAYERS: u32 = 2;
/// What one texture slot is, GPU-side. Parallel to `Textures`' own slot
/// numbering (`TextureKind`'s `Image`/`Page`), so a slot's index means the
/// same thing on both sides without a second map to keep in sync.
@@ -360,7 +364,26 @@ impl GpuTextures {
})
}
/// The atlas is sampled as a `texture_2d_array`, and **a one-layer
/// array is not one on the GLES backend**: wgpu-hal picks the GL
/// texture target from the descriptor alone
/// (`gles::Texture::get_info_from_desc`, `(false, 1) => TEXTURE_2D`),
/// so a capacity of 1 creates a `GL_TEXTURE_2D` and binds it to the
/// shader's `sampler2DArray`. GL then treats that unit as incomplete
/// and every `textureSample` returns (0, 0, 0, 1) -- which, through
/// `draw_glyph`'s `color.a *= texel.a`, draws every glyph as a solid
/// filled box. That was iris's appearance on the emulator's GLES for
/// two days (RUST.md, "the emulator cannot draw iris's glyphs"), and
/// it is a real defect on any device whose adapter is GL rather than
/// Vulkan, not an emulator artifact. So the array never has fewer than
/// `MIN_ARRAY_LAYERS` layers; the second layer costs one page of
/// texture memory and is used by the next atlas page anyway.
fn create_array_texture(device: &Device, capacity: u32) -> Texture {
debug_assert!(
capacity >= MIN_ARRAY_LAYERS,
"glyph atlas array asked for {capacity} layers; fewer than {MIN_ARRAY_LAYERS} is a \
GL_TEXTURE_2D on the GLES backend and draws every glyph as a box"
);
device.create_texture(&TextureDescriptor {
label: Some("glyph atlas array"),
size: Extent3d {
@@ -382,7 +405,7 @@ impl GpuTextures {
pub fn new(device: &Device, queue: &Queue) -> Self {
let sampler = default_sampler(device);
let null_view = null_texture_view(device);
let array_capacity = 1;
let array_capacity = MIN_ARRAY_LAYERS;
let array_texture = Self::create_array_texture(device, array_capacity);
let array_view = array_texture.create_view(&TextureViewDescriptor {
dimension: Some(TextureViewDimension::D2Array),
+10 -1
View File
@@ -85,7 +85,16 @@ impl UiRenderer {
let size = window.inner_size();
let instance = Instance::new(&InstanceDescriptor {
backends: Backends::PRIMARY,
// `force-gles` on the desktop too, not just on Android: the
// GLES backend has behaviour of its own (a one-layer array
// texture is a `GL_TEXTURE_2D` -- see
// `GpuTextures::create_array_texture`), and a machine with a
// real GPU is where that is cheap to reproduce and screenshot.
backends: if cfg!(feature = "force-gles") {
Backends::GL
} else {
Backends::PRIMARY
},
..Default::default()
});