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:
1 parent
69525bd131
commit
3cb18ac5c2
5 files changed
+108
-14
No files matched your search
+14
-11
@@ -609,17 +609,20 @@ agent ticks it here with the evidence.
|
|||||||
format rather than the palette -- but it makes the desktop build
|
format rather than the palette -- but it makes the desktop build
|
||||||
useless as a colour reference, which is exactly what P1a needed it for
|
useless as a colour reference, which is exactly what P1a needed it for
|
||||||
when the emulator could not draw glyphs.
|
when the emulator could not draw glyphs.
|
||||||
- [ ] **The emulator cannot draw iris's glyphs.** Under `-gpu host` with
|
- [x] **Every glyph was a solid box on the GLES backend -- iris's bug,
|
||||||
Vulkan disabled (Mesa 26.2.2 / virgl -- what `emu` does on this
|
not the emulator's.** Fixed 2026-09-06. The atlas is one
|
||||||
machine) every character renders as a solid filled box: the atlas
|
`texture_2d_array` and `GpuTextures::new` created it with **one
|
||||||
sample's alpha reads 1, which is what an incomplete GL texture returns
|
layer**; wgpu-hal picks the GL target from the descriptor
|
||||||
(0,0,0,1). Not new (`20303e0` does it too) and not the platform's
|
(`(false, 1) => TEXTURE_2D`), so under GLES that array was a
|
||||||
(Compose draws text perfectly on the same AVD in the same minute).
|
`GL_TEXTURE_2D` bound to the shader's `sampler2DArray`, the unit was
|
||||||
Enabling host Vulkan still dies at boot in gfxstream, and
|
incomplete, every `textureSample` returned (0,0,0,1), and
|
||||||
`EMU_GPU=software` gives SwiftShader Vulkan on which iris **SIGSEGVs
|
`draw_glyph`'s `color.a *= texel.a` filled the quad. `MIN_ARRAY_LAYERS
|
||||||
in `surface_changed`**. Either of the last two would restore
|
= 2` in `iris/core/src/render/texture.rs`, with a `debug_assert!` at
|
||||||
appearance testing on Android; today it has to be done on the desktop
|
`create_array_texture`. Vulkan (the phone, the desktop's default
|
||||||
backend or on Iris's phone.
|
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)
|
## Build (for the port)
|
||||||
|
|
||||||
|
|||||||
@@ -5600,6 +5600,61 @@ device.
|
|||||||
transcript-ui -p desktop-app -p tabs-ui --all-targets`
|
transcript-ui -p desktop-app -p tabs-ui --all-targets`
|
||||||
warning-free; `cargo test` 85 (iris, +4) + 13 (iris-core) +
|
warning-free; `cargo test` 85 (iris, +4) + 13 (iris-core) +
|
||||||
31 (transcript-ui, +11) + 123 (client-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`/
|
- [ ] **P1b — tool-call cards and grouping.** `ToolRows.kt`/
|
||||||
`ToolInput.kt`'s cards: a collapsed row per call with name
|
`ToolInput.kt`'s cards: a collapsed row per call with name
|
||||||
and a one-line summary, expand to input and output, runs of
|
and a one-line summary, expand to input and output, runs of
|
||||||
|
|||||||
+5
-1
@@ -66,7 +66,11 @@ send_wrapper = "0.6.0"
|
|||||||
# default) or virgl's GLES path, without a second env-var plumbing path that
|
# 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
|
# 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
|
# (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 = []
|
force-gles = []
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ use crate::{PatchRect, TextureKind, TextureUpdate, Textures};
|
|||||||
|
|
||||||
use super::atlas::PAGE;
|
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
|
/// What one texture slot is, GPU-side. Parallel to `Textures`' own slot
|
||||||
/// numbering (`TextureKind`'s `Image`/`Page`), so a slot's index means the
|
/// 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.
|
/// 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 {
|
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 {
|
device.create_texture(&TextureDescriptor {
|
||||||
label: Some("glyph atlas array"),
|
label: Some("glyph atlas array"),
|
||||||
size: Extent3d {
|
size: Extent3d {
|
||||||
@@ -382,7 +405,7 @@ impl GpuTextures {
|
|||||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||||
let sampler = default_sampler(device);
|
let sampler = default_sampler(device);
|
||||||
let null_view = null_texture_view(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_texture = Self::create_array_texture(device, array_capacity);
|
||||||
let array_view = array_texture.create_view(&TextureViewDescriptor {
|
let array_view = array_texture.create_view(&TextureViewDescriptor {
|
||||||
dimension: Some(TextureViewDimension::D2Array),
|
dimension: Some(TextureViewDimension::D2Array),
|
||||||
|
|||||||
@@ -85,7 +85,16 @@ impl UiRenderer {
|
|||||||
let size = window.inner_size();
|
let size = window.inner_size();
|
||||||
|
|
||||||
let instance = Instance::new(&InstanceDescriptor {
|
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()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user