RUST.md: iris's bindless texture array does not survive real Android GPUs

Iris asked whether the 'unknown number of images' approach even works on
mobile. It does not, measured with a new rig (rigs/gpu-probe, no APK
needed) and sourced rather than recalled: the emulator's software Vulkan
refuses iris's descriptor-indexing request outright, and on real hardware
the current Android Vulkan Profile baseline (80.1% of active devices)
does not require VK_EXT_descriptor_indexing either -- Arm's own docs say
only Valhall/5th-Gen Mali (2019+) support it.

iris already solved the identical problem for text in I1 (the glyph
atlas). The recommendation is to generalize it to images rather than
widen the binding array further; not yet implemented, since it changes
iris's render core.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Sonnet 5 committed 2026-09-04 21:17:18 -04:00
1 parent c70a670356
commit 79b9cd789a
6 files changed
+1592 -12

No files matched your search

+1
View File
@@ -0,0 +1 @@
target
+1188
View File
File diff suppressed because it is too large. Load diff
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "gpu-probe"
version = "0.1.0"
edition = "2024"
# Deliberately its own crate rather than a member of iris's workspace: the
# vendored `iris/` tree is meant to stay reconcilable with the iris/iris
# repository, and this is a rig belonging to ai-app.
[dependencies]
# Pinned to what iris asks for, so the answer is about iris rather than
# about a different wgpu.
wgpu = "28.0.0"
pollster = "0.4.0"
# Queried directly, because wgpu and `cmd gpu vkjson` disagreed about
# descriptor indexing in the emulator and only the raw call says which is
# right.
ash = "0.38"
+154
View File
@@ -0,0 +1,154 @@
//! 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.
//!
//! 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
//! `adb push` and run from `/data/local/tmp`, which is far cheaper than an
//! app. What it therefore cannot answer is anything about presenting to a
//! surface; that is the Android backend's own problem.
mod vk;
use wgpu::*;
/// What `iris/src/default/render.rs` asks `request_device` for.
fn iris_features() -> Features {
Features::TEXTURE_BINDING_ARRAY
| Features::PARTIALLY_BOUND_BINDING_ARRAY
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING
}
/// `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;
fn main() {
vk::report();
let instance = Instance::new(&InstanceDescriptor {
backends: Backends::from_env().unwrap_or(Backends::PRIMARY),
..Default::default()
});
let adapters = pollster::block_on(instance.enumerate_adapters(Backends::all()));
println!("adapters: {}", adapters.len());
for adapter in &adapters {
let info = adapter.get_info();
println!(
" {:?} {} ({:?})",
info.backend, info.name, info.device_type
);
}
let Some(adapter) = pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
power_preference: PowerPreference::default(),
compatible_surface: None,
force_fallback_adapter: false,
}))
.ok() else {
println!("\nNO ADAPTER");
std::process::exit(1);
};
let info = adapter.get_info();
println!(
"\nchosen: {:?} {} ({:?})",
info.backend, info.name, info.device_type
);
println!("driver: {} {}", info.driver, info.driver_info);
let have = adapter.features();
println!("\nfeatures iris requires:");
let mut missing = Features::empty();
for f in iris_features().iter() {
let ok = have.contains(f);
println!(
" {:60} {}",
format!("{f:?}"),
if ok { "yes" } else { "NO" }
);
if !ok {
missing |= f;
}
}
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
);
// 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,
..Default::default()
};
match pollster::block_on(adapter.request_device(&DeviceDescriptor {
required_features: iris_features(),
required_limits: wanted.clone(),
..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;
}
}
}
}
+108
View File
@@ -0,0 +1,108 @@
//! The raw Vulkan half of the probe.
//!
//! wgpu reports a feature only after a chain of its own decisions -- which
//! physical device, which API version, which extension list -- so "wgpu says
//! no" and "the driver says no" are different claims. This asks
//! `vkGetPhysicalDeviceFeatures2` itself and prints the inputs to that chain,
//! so a disagreement can be attributed rather than guessed at.
use ash::{Entry, vk};
use std::ffi::CStr;
pub fn report() {
let entry = match unsafe { Entry::load() } {
Ok(e) => e,
Err(e) => {
println!("\nraw vulkan: cannot load loader -- {e}");
return;
}
};
let instance_version = match unsafe { entry.try_enumerate_instance_version() } {
Ok(Some(v)) => v,
Ok(None) => vk::API_VERSION_1_0,
Err(e) => {
println!("\nraw vulkan: enumerate_instance_version failed -- {e}");
return;
}
};
println!("\nraw vulkan:");
println!(" loader instance version: {}", ver(instance_version));
// Ask for the highest instance version the loader admits to: wgpu clamps
// the device version by the instance's, so an instance created at 1.0
// makes a 1.3 device look like 1.0.
let app_info = vk::ApplicationInfo::default().api_version(instance_version);
let create = vk::InstanceCreateInfo::default().application_info(&app_info);
let instance = match unsafe { entry.create_instance(&create, None) } {
Ok(i) => i,
Err(e) => {
println!(" create_instance failed -- {e}");
return;
}
};
let devices = unsafe { instance.enumerate_physical_devices() }.unwrap_or_default();
for phd in devices {
let props = unsafe { instance.get_physical_device_properties(phd) };
let name = unsafe { CStr::from_ptr(props.device_name.as_ptr()) };
println!(" device: {}", name.to_string_lossy());
println!(" device api version: {}", ver(props.api_version));
let exts =
unsafe { instance.enumerate_device_extension_properties(phd) }.unwrap_or_default();
let has_ext = exts.iter().any(|e| {
(unsafe { CStr::from_ptr(e.extension_name.as_ptr()) }) == c"VK_EXT_descriptor_indexing"
});
println!(" VK_EXT_descriptor_indexing advertised: {has_ext}");
println!(" device extensions: {}", exts.len());
let mut indexing = vk::PhysicalDeviceDescriptorIndexingFeatures::default();
let mut features2 = vk::PhysicalDeviceFeatures2::default().push_next(&mut indexing);
unsafe { instance.get_physical_device_features2(phd, &mut features2) };
for (name, v) in [
(
"shaderSampledImageArrayNonUniformIndexing",
indexing.shader_sampled_image_array_non_uniform_indexing,
),
(
"descriptorBindingSampledImageUpdateAfterBind",
indexing.descriptor_binding_sampled_image_update_after_bind,
),
(
"shaderStorageImageArrayNonUniformIndexing",
indexing.shader_storage_image_array_non_uniform_indexing,
),
(
"descriptorBindingStorageImageUpdateAfterBind",
indexing.descriptor_binding_storage_image_update_after_bind,
),
(
"shaderStorageBufferArrayNonUniformIndexing",
indexing.shader_storage_buffer_array_non_uniform_indexing,
),
(
"descriptorBindingStorageBufferUpdateAfterBind",
indexing.descriptor_binding_storage_buffer_update_after_bind,
),
(
"descriptorBindingPartiallyBound",
indexing.descriptor_binding_partially_bound,
),
] {
println!(" {name:48} {}", if v != 0 { "yes" } else { "NO" });
}
}
unsafe { instance.destroy_instance(None) };
}
fn ver(v: u32) -> String {
format!(
"{}.{}.{}",
vk::api_version_major(v),
vk::api_version_minor(v),
vk::api_version_patch(v)
)
}