iris is the framework alone; the app is one crate in app-rust/
Iris: "the organization of the rust rewrite is a mess right now... there shouldn't be anything related to the app inside of iris. Iris is supposed to be the UI framework alone." And, on the crate count: "I'm confused why the app only code needs more than one crate though." Nine cargo workspaces become three, and the port's project code -- which sat in five places, four of them inside the framework -- becomes one crate, `ai-app`, in `app-rust/`: client-core -> app-rust/src/client iris/transcript-ui -> app-rust/src/ui iris/transcript-fixture -> app-rust/src/ui/fixture.rs + tests/ + touch/ iris/desktop-app -> app-rust/src/desktop + src/bin_desktop.rs iris/android-app -> app-rust/src/android + android-project/ android-shell -> app-rust/src/shell iris/ keeps core, macro, the iris crate, tabs-ui and rig-input, and now mentions no session, transcript, setup or server anywhere. Only two of the old splits had a reason that survived reading. event-model stays a crate at the repo root because server/ depends on it too, so a crate is what makes the backend and the app agree by construction. The two Android .so names looked like a hard constraint -- a package produces one library artifact -- until P2 turned out to already plan merging those two Android apps into one; both faces now come out of libai_app.so, picked apart by features so `--no-default-features --features shell` keeps wgpu, parley and iris out of the Compose app's APK. docs/RUST.md's "One app crate" has the rest, including what each remaining feature is for. DECISIONS.md and SUBAGENTS.md move into docs/ with everything else. Verified: ./run-tests.sh and `cd iris && cargo test` green, clippy and fmt clean in all five workspaces, `cargo ndk -t x86_64` links libai_app.so, build-apk.sh produces an APK that installs and launches on this checkout's emulator (Gl ... virgl, as expected), and the phone-sized headless screenshot renders the transcript unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b54aaf3c4
commit
a9312e9431
113 files changed
+23221
-2992
No files matched your search
+125
-22
@@ -1,4 +1,5 @@
|
||||
use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState};
|
||||
use crate::task::RequestRedraw;
|
||||
use iris_core::{UiData, UiRenderNode, UiRenderState, util::Vec2};
|
||||
use pollster::FutureExt;
|
||||
use std::sync::Arc;
|
||||
use wgpu::*;
|
||||
@@ -6,6 +7,12 @@ use winit::{dpi::PhysicalSize, window::Window};
|
||||
|
||||
pub const CLEAR_COLOR: Color = Color::BLACK;
|
||||
|
||||
impl RequestRedraw for Window {
|
||||
fn request_redraw(&self) {
|
||||
Window::request_redraw(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UiRenderer {
|
||||
window: Arc<Window>,
|
||||
surface: Surface<'static>,
|
||||
@@ -22,7 +29,16 @@ impl UiRenderer {
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
let output = self.surface.get_current_texture().unwrap();
|
||||
let output = match self.surface.get_current_texture() {
|
||||
CurrentSurfaceTexture::Success(texture)
|
||||
| CurrentSurfaceTexture::Suboptimal(texture) => texture,
|
||||
// wgpu 30 turned this Result into an enum; every arm here was an
|
||||
// `Err` the previous `.unwrap()` panicked on, except `Occluded`,
|
||||
// which is new. Named rather than swallowed: a window that stops
|
||||
// presenting silently is the state this file's `pre_present_notify`
|
||||
// comment was written about.
|
||||
other => panic!("no surface texture to draw into: {other:?}"),
|
||||
};
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&TextureViewDescriptor::default());
|
||||
@@ -45,14 +61,25 @@ impl UiRenderer {
|
||||
}
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
output.present();
|
||||
// Immediately before presenting, so the windowing system can schedule
|
||||
// the frame. On Wayland this is what ties the commit to the surface's
|
||||
// frame callback; without it a frame drawn when nothing else follows
|
||||
// could sit unpresented, and the window kept the layout it had before
|
||||
// the compositor's first resize -- intermittently, on about a fifth of
|
||||
// starts, with nothing left to flush it.
|
||||
self.window.pre_present_notify();
|
||||
self.queue.present(output);
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
||||
self.config.width = size.width;
|
||||
self.config.height = size.height;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.ui.resize(size, &self.queue);
|
||||
// Physical, matching `new`'s own seed -- see the comment there.
|
||||
self.ui.resize(
|
||||
Vec2::new(size.width as f32, size.height as f32),
|
||||
&self.queue,
|
||||
);
|
||||
}
|
||||
|
||||
fn create_encoder(device: &Device) -> CommandEncoder {
|
||||
@@ -64,10 +91,44 @@ impl UiRenderer {
|
||||
pub fn new(window: Arc<Window>) -> Self {
|
||||
let size = window.inner_size();
|
||||
|
||||
let instance = Instance::new(&InstanceDescriptor {
|
||||
backends: Backends::PRIMARY,
|
||||
..Default::default()
|
||||
// `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.
|
||||
let mut backends = if cfg!(feature = "force-gles") {
|
||||
Backends::GL
|
||||
} else {
|
||||
Backends::PRIMARY
|
||||
};
|
||||
// The display handle comes from the window rather than being left
|
||||
// out: wgpu 30 asks for it whenever a GLES surface is going to be
|
||||
// presented on Wayland, which is exactly what the fallback below
|
||||
// produces on this machine.
|
||||
let mut instance = Instance::new(InstanceDescriptor {
|
||||
backends,
|
||||
..InstanceDescriptor::new_with_display_handle(Box::new(window.clone()))
|
||||
});
|
||||
// The same fallback the Android backend grew in 85869d0, and for
|
||||
// the same reason: a machine can advertise a Vulkan ICD with no
|
||||
// device behind it, and refusing to draw at all because the only
|
||||
// usable adapter is a GLES one is iris's bug rather than the
|
||||
// machine's. On this VM the Vulkan device disappears whenever
|
||||
// the host refuses a virtio-gpu context, so `run-headless.sh` --
|
||||
// layer 2 of the test rig -- aborted with `Could not get
|
||||
// adapter!` while GL was sitting there working. Probed before the
|
||||
// surface exists, matching Android, where an instance carrying
|
||||
// both backends fails worse than one carrying the wrong one.
|
||||
if backends != Backends::GL && instance.enumerate_adapters(backends).block_on().is_empty() {
|
||||
log::warn!(
|
||||
"iris renderer: no {backends:?} adapter on this machine, falling back to GLES"
|
||||
);
|
||||
backends = Backends::GL;
|
||||
instance = Instance::new(InstanceDescriptor {
|
||||
backends,
|
||||
..InstanceDescriptor::new_with_display_handle(Box::new(window.clone()))
|
||||
});
|
||||
}
|
||||
|
||||
let surface = instance
|
||||
.create_surface(window.clone())
|
||||
@@ -78,25 +139,48 @@ impl UiRenderer {
|
||||
power_preference: PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
..Default::default()
|
||||
})
|
||||
.block_on()
|
||||
.expect("Could not get adapter!");
|
||||
.unwrap_or_else(|error| {
|
||||
panic!("No usable GPU adapter for backends {backends:?}: {error}")
|
||||
});
|
||||
|
||||
let ui_limits = UiLimits::default();
|
||||
// Say which adapter won, in the same words the Android backend
|
||||
// uses. Without it a layer-2 screenshot or frame time from this
|
||||
// window carries no record of what drew it, and the two cases that
|
||||
// matter look identical in the PNG: the host's real GPU, and
|
||||
// llvmpipe after this VM lost its virtio-gpu contexts. That
|
||||
// happened on 2026-09-08, and the only reason anyone noticed is
|
||||
// that the fallback above did not exist yet and the app aborted
|
||||
// instead. A silent fallback needs this line to stay honest.
|
||||
{
|
||||
let info = adapter.get_info();
|
||||
log::info!(
|
||||
"iris renderer: {name} ({backend:?}, {driver}{driver_info}) on {backends:?}",
|
||||
name = info.name,
|
||||
backend = info.backend,
|
||||
driver = info.driver,
|
||||
driver_info = if info.driver_info.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {}", info.driver_info)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// No features beyond what wgpu asks for by default, and no
|
||||
// binding-array limits: the atlas is one texture_2d_array and a
|
||||
// standalone image is its own ordinary bind group, neither of which
|
||||
// needs descriptor indexing. See TEXTURES.md's "Recommended shape"
|
||||
// for why the old binding array asked for
|
||||
// VK_EXT_descriptor_indexing unconditionally and did not survive a
|
||||
// real share of Android GPUs. `iris_core::device_limits()` is
|
||||
// shared with the Android backend; see its own doc for why it is
|
||||
// not simply `Limits::default()`.
|
||||
let (device, queue) = adapter
|
||||
.request_device(&DeviceDescriptor {
|
||||
required_features: Features::TEXTURE_BINDING_ARRAY
|
||||
| Features::PARTIALLY_BOUND_BINDING_ARRAY
|
||||
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
|
||||
required_limits: Limits {
|
||||
max_binding_array_elements_per_shader_stage: ui_limits
|
||||
.max_binding_array_elements_per_shader_stage(),
|
||||
max_binding_array_sampler_elements_per_shader_stage: ui_limits
|
||||
.max_binding_array_sampler_elements_per_shader_stage(),
|
||||
max_buffer_size: 1 << 30,
|
||||
..Default::default()
|
||||
},
|
||||
required_limits: iris_core::device_limits(),
|
||||
..Default::default()
|
||||
})
|
||||
.block_on()
|
||||
@@ -113,9 +197,16 @@ impl UiRenderer {
|
||||
let config = SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
// wgpu 30's new field; `Auto` is what every earlier version did.
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: PresentMode::AutoNoVsync,
|
||||
// Vsync, because a toolkit aiming at battery life must not present
|
||||
// frames a display will never show: AutoNoVsync accepts them as
|
||||
// fast as the GPU will take them, so a redraw burst costs whatever
|
||||
// the hardware can be made to do rather than one frame.
|
||||
// AutoVsync picks Fifo, which every backend supports.
|
||||
present_mode: PresentMode::AutoVsync,
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
desired_maximum_frame_latency: 2,
|
||||
view_formats: vec![],
|
||||
@@ -125,7 +216,19 @@ impl UiRenderer {
|
||||
|
||||
let encoder = Self::create_encoder(&device);
|
||||
|
||||
let ui = UiRenderNode::new(&device, &queue, &config, ui_limits);
|
||||
// Unlike the Android backend, the desktop backend has no on-screen
|
||||
// fallback to show a diagnostic through, so a renderer-creation
|
||||
// failure still panics here -- but now with wgpu's full "Caused
|
||||
// by:" chain as the message, since `UiRenderNode::new` returns it
|
||||
// rather than letting wgpu's own default handler panic first (see
|
||||
// that function's doc comment).
|
||||
// Physical size, the same units the swapchain, `WindowEvent::
|
||||
// Resized`, the pointer and the widget tree all use -- see
|
||||
// `default::content_scale` for why this backend stopped dividing
|
||||
// into a separate logical space, and what disagreed while it did.
|
||||
let physical_size = Vec2::new(size.width as f32, size.height as f32);
|
||||
let ui = UiRenderNode::new(&device, &queue, &config, physical_size)
|
||||
.expect("Could not create iris render node!");
|
||||
|
||||
Self {
|
||||
surface,
|
||||
|
||||
Reference in new issue
Block a user