iris: replace the bindless texture array with an atlas array + per-image bind groups

The old pipeline bound every texture ever drawn (glyph atlas pages and
standalone images alike) in one binding_array<texture_2d<f32>> and asked
every device, unconditionally, for VK_EXT_descriptor_indexing -- which a
real share of Android GPUs lack and which failed outright on the Android
emulator's software Vulkan (see TEXTURES.md's "iris's binding array does
not survive real Android hardware").

Implements TEXTURES.md's "Recommended shape": the glyph atlas is now one
texture_2d_array (a layer per page, grown by doubling + GPU-side
copy_texture_to_texture); a standalone image is its own ordinary Texture
and BindGroup, drawn with its own draw() call from a separate per-layer
instance list; group 2's layout is {atlas array, one image slot, sampler,
masks}. request_device now asks for no features and no binding-array
limits at all, and UiLimits is gone.

Also fixes (by making moot) the changed=false bug the review found, where
a Patch in the same batch could cancel an earlier Push's rebuild signal,
and documents the swap_remove draw-order invariant apply_free already
relied on.

Verified: cargo fmt/build/clippy/test clean in iris/ on the pinned
nightly; minimal and tabs render correctly via run-headless.sh; a
throwaway example confirmed the standalone-image bind-group path renders;
rigs/gpu-probe, updated to the new empty feature/limit set, confirms
request_device succeeds on the ai-app-2 emulator's software Vulkan
(EMU_GPU=software) -- see TEXTURES.md's "Implemented, 2026-09-04" for the
exact command and output. RUST.md's blocking item is resolved.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
This commit is contained in:
irisandClaude Sonnet committed 2026-09-04 22:28:54 -04:00
1 parent 1c937e2f48
commit e0a473e090
14 files changed
+1046 -332

No files matched your search

+38 -69
View File
@@ -1,11 +1,19 @@
//! Ask a device whether it can give iris the GPU it asks for.
//!
//! iris's renderer binds every texture it has drawn as one binding array and
//! indexes it non-uniformly from the shader, which needs descriptor indexing
//! and a very large per-stage binding-array limit (101,000 elements: 100,000
//! textures and 1,000 samplers, `UiLimits::default`). Those are ordinary on a
//! desktop and not obviously available on a phone, so this reports what the
//! adapter offers before anything is built on the assumption.
//! Until 2026-09-04 iris's renderer bound every texture it had drawn as one
//! binding array and indexed it non-uniformly from the shader, which needed
//! descriptor indexing and a very large per-stage binding-array limit
//! (101,000 elements: 100,000 textures and 1,000 samplers,
//! `UiLimits::default`). That was ordinary on a desktop and, per
//! TEXTURES.md's "iris's binding array does not survive real Android
//! hardware", not available on a real share of Android GPUs -- and it failed
//! outright on this emulator's software Vulkan, which is what this rig
//! caught first. iris now asks for nothing beyond wgpu's own defaults (see
//! `iris/src/default/render.rs`): the glyph atlas is one `texture_2d_array`
//! and a standalone image is its own ordinary bind group, and neither needs
//! descriptor indexing. This rig still asks `request_device` for exactly
//! what iris asks for, so it keeps being the answer to "does iris's actual
//! device request succeed here" rather than a guess from reading the code.
//!
//! It runs as a plain executable with no window and no APK, because
//! `request_adapter` needs no surface -- so it can be pushed to a device with
@@ -17,16 +25,15 @@ mod vk;
use wgpu::*;
/// What `iris/src/default/render.rs` asks `request_device` for.
/// What `iris/src/default/render.rs` asks `request_device` for, now that the
/// binding array is gone: nothing beyond wgpu's own default feature set.
fn iris_features() -> Features {
Features::TEXTURE_BINDING_ARRAY
| Features::PARTIALLY_BOUND_BINDING_ARRAY
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
Features::empty()
}
/// `UiLimits::default()`: 100,000 textures + 1,000 samplers.
const IRIS_MAX_BINDING_ARRAY: u32 = 101_000;
const IRIS_MAX_BINDING_ARRAY_SAMPLERS: u32 = 1_000;
/// The one non-default limit iris asks for -- unrelated to the binding array,
/// kept for the big storage buffers behind rects/glyphs.
const IRIS_MAX_BUFFER_SIZE: u64 = 1 << 30;
fn main() {
vk::report();
@@ -80,75 +87,37 @@ fn main() {
let limits = adapter.limits();
println!("\nlimits iris requires:");
for (name, want, got) in [
(
"max_binding_array_elements_per_shader_stage",
IRIS_MAX_BINDING_ARRAY,
limits.max_binding_array_elements_per_shader_stage,
),
(
"max_binding_array_sampler_elements_per_shader_stage",
IRIS_MAX_BINDING_ARRAY_SAMPLERS,
limits.max_binding_array_sampler_elements_per_shader_stage,
),
] {
println!(
" {name:52} want {want:>7} have {got:>7} {}",
if got >= want { "ok" } else { "TOO SMALL" }
);
}
println!(
" {:52} want {:>7} have {:>7}",
"max_buffer_size (iris asks 1<<30)",
1u64 << 30,
limits.max_buffer_size
" {:52} want {:>7} have {:>7} {}",
"max_buffer_size",
IRIS_MAX_BUFFER_SIZE,
limits.max_buffer_size,
if limits.max_buffer_size >= IRIS_MAX_BUFFER_SIZE {
"ok"
} else {
"TOO SMALL"
}
);
// The question that actually matters: does the device iris builds come
// back, or does wgpu refuse it?
let mut wanted = Limits {
max_binding_array_elements_per_shader_stage: IRIS_MAX_BINDING_ARRAY,
max_binding_array_sampler_elements_per_shader_stage: IRIS_MAX_BINDING_ARRAY_SAMPLERS,
max_buffer_size: 1 << 30,
// back, or does wgpu refuse it? With no features and no binding-array
// limits requested, this is expected to succeed everywhere -- this rig
// is what turned that from an assumption into a measurement, first on
// this emulator's software Vulkan.
let wanted = Limits {
max_buffer_size: IRIS_MAX_BUFFER_SIZE,
..Default::default()
};
match pollster::block_on(adapter.request_device(&DeviceDescriptor {
required_features: iris_features(),
required_limits: wanted.clone(),
required_limits: wanted,
..Default::default()
})) {
Ok(_) => println!("\nIRIS DEVICE: ok"),
Err(e) => println!("\nIRIS DEVICE: FAILED -- {e}"),
}
// If it failed, say how far down it has to be turned before it works, so
// the report names a number to design against rather than just "no".
if missing.is_empty() {
for cap in [
limits.max_binding_array_elements_per_shader_stage,
1024,
128,
16,
] {
if cap >= IRIS_MAX_BINDING_ARRAY {
continue;
}
wanted.max_binding_array_elements_per_shader_stage = cap;
wanted.max_binding_array_sampler_elements_per_shader_stage =
cap.min(IRIS_MAX_BINDING_ARRAY_SAMPLERS);
let ok = pollster::block_on(adapter.request_device(&DeviceDescriptor {
required_features: iris_features(),
required_limits: wanted.clone(),
..Default::default()
}))
.is_ok();
println!(
" binding array capped at {cap:>7}: {}",
if ok { "ok" } else { "no" }
);
if ok {
break;
}
}
if !missing.is_empty() {
println!("\nmissing features: {missing:?}");
}
}