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
@@ -0,0 +1,526 @@
|
||||
use crate::task::RequestRedraw;
|
||||
use android_view::{
|
||||
View,
|
||||
jni::{JavaVM, objects::GlobalRef},
|
||||
ndk::native_window::NativeWindow,
|
||||
};
|
||||
use iris_core::{UiData, UiRenderNode, UiRenderState};
|
||||
use pollster::FutureExt;
|
||||
use std::time::{Duration, Instant};
|
||||
use wgpu::{
|
||||
rwh::{DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle},
|
||||
*,
|
||||
};
|
||||
|
||||
pub const CLEAR_COLOR: Color = Color::BLACK;
|
||||
|
||||
/// `NativeWindow` (from the surface android-view hands over in
|
||||
/// `surfaceChanged`) has a window handle but not a display one -- there is
|
||||
/// exactly one display on Android and `rwh` has a unit variant for it.
|
||||
/// Mirrors android-view's own demo (`demo/src/lib.rs`'s
|
||||
/// `AndroidWindowHandle`).
|
||||
struct AndroidWindowHandle {
|
||||
window: NativeWindow,
|
||||
}
|
||||
|
||||
impl HasDisplayHandle for AndroidWindowHandle {
|
||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||
Ok(DisplayHandle::android())
|
||||
}
|
||||
}
|
||||
|
||||
impl HasWindowHandle for AndroidWindowHandle {
|
||||
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
||||
self.window.window_handle()
|
||||
}
|
||||
}
|
||||
|
||||
/// The android-view surface, unlike winit's window, does not outlive a
|
||||
/// backgrounding of the activity: `surfaceDestroyed`/`surfaceCreated` (via
|
||||
/// `SurfaceHolder.Callback`) recreate it, so this holds everything that
|
||||
/// depends on that surface rather than being built once at startup --
|
||||
/// `AndroidUiState` holds it as `Option<AndroidRenderer>`, `None` exactly
|
||||
/// when there is no surface to draw into.
|
||||
pub struct AndroidRenderer {
|
||||
surface: Surface<'static>,
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
config: SurfaceConfiguration,
|
||||
encoder: CommandEncoder,
|
||||
pub ui: UiRenderNode,
|
||||
/// The adapter identity, kept past `new()` for the Diagnostics page --
|
||||
/// `Adapter` itself is not `Clone`, so the three fields the page shows
|
||||
/// are copied out once here rather than holding the adapter.
|
||||
pub adapter_name: String,
|
||||
pub adapter_backend: Backend,
|
||||
pub adapter_driver: String,
|
||||
/// Every uncaptured wgpu error since this renderer was created -- see
|
||||
/// `iris_core::WgpuErrorLog`'s doc comment. Installed on `device` in
|
||||
/// `new()`, kept here so the Diagnostics page and the per-frame log in
|
||||
/// `update()` can both read it without a global.
|
||||
pub wgpu_errors: iris_core::WgpuErrorLog,
|
||||
/// Frames drawn on this surface -- what gates the first-10-frames log
|
||||
/// `update()` writes (RUST.md's P0 box, "the first input frame"
|
||||
/// investigation): a fresh surface is exactly what Iris's own report
|
||||
/// says renders correctly at first, so the frames that matter are the
|
||||
/// first several after each `surface_changed`, not an arbitrary window
|
||||
/// during a long-running session.
|
||||
frame_count: u64,
|
||||
/// Physical pixels per dp -- see `android::view::AndroidUiState::
|
||||
/// content_scale`'s field comment for what this feeds.
|
||||
content_scale: f32,
|
||||
}
|
||||
|
||||
/// One frame's worth of the counters `render/mod.rs`'s doc comments on
|
||||
/// `FrameUpdateStats`/`take_image_bind_group_creates`/
|
||||
/// `take_atlas_pages_grown` describe -- assembled here because the three
|
||||
/// live on two different calling conventions (`FrameUpdateStats` from this
|
||||
/// exact `update()` call; the other two describe the *previous* frame,
|
||||
/// same as `bench_images`' existing use of them) and a diagnostic reader
|
||||
/// should not have to know that split.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct FrameDiagnostics {
|
||||
pub masks_resized: bool,
|
||||
pub moves_resized: bool,
|
||||
/// From the previous frame's `update()` -- see the struct doc.
|
||||
pub atlas_pages_grown_prev: u64,
|
||||
pub image_bind_group_creates_prev: u64,
|
||||
}
|
||||
|
||||
impl AndroidRenderer {
|
||||
/// `Err` holds a full, human-readable report for **every** way this
|
||||
/// can fail -- no surface, no adapter, no device, or wgpu's own error
|
||||
/// text (`UiRenderNode::new`'s doc comment) plus the adapter identity
|
||||
/// and the limits/downlevel flags bind-group-layout validation checks
|
||||
/// against -- rather than the panic wgpu's default error handler would
|
||||
/// otherwise raise with no caller able to see it. This is what aborted
|
||||
/// the P0 bench APK on Iris's phone with only "wgpu error: Validation
|
||||
/// Error" surviving into the crash report (RUST.md's P0 box, "iris
|
||||
/// bench crash on the phone, 2026-09-06"): `create_bind_group_layout`
|
||||
/// validates against *this* adapter's downlevel capabilities and
|
||||
/// limits, which a desktop GPU and the emulator's software renderers
|
||||
/// never exercised. The caller (`android::view::IrisViewPeer::
|
||||
/// surface_changed`) logs this one-line-flattened and shows it on
|
||||
/// screen instead of aborting the process.
|
||||
pub fn new(
|
||||
window: NativeWindow,
|
||||
width: u32,
|
||||
height: u32,
|
||||
content_scale: f32,
|
||||
) -> Result<Self, String> {
|
||||
// `force-gles` (RUST.md's I5 "Where iris's frame time goes") pins
|
||||
// the build to GLES, to isolate whether the backend itself explains
|
||||
// the frame time gap against Compose. `cfg!` rather than a runtime
|
||||
// switch: there is no way to hand an env var to an already-launched
|
||||
// Android process on this machine (see the feature's doc in
|
||||
// Cargo.toml).
|
||||
//
|
||||
// Otherwise: **Vulkan where it has an adapter at all, GLES where it
|
||||
// has none.** `Backends::PRIMARY` leaves `GL` out, so a device
|
||||
// offering only a GLES adapter had no adapter at all and this
|
||||
// function aborted the process -- this checkout's emulator, whose
|
||||
// Vulkan ICD carries no adapter behind it (`NotFound {
|
||||
// active_backends: VULKAN, no_adapter_backends: VULKAN,
|
||||
// supported_backends: VULKAN | GL }`), and the crash loop in
|
||||
// RUST.md's queue.
|
||||
//
|
||||
// The choice is made *before any surface exists*, with an instance
|
||||
// that never touches the window, because **an Android window can be
|
||||
// connected to one graphics API only**. One instance carrying both
|
||||
// backends does not work: `create_surface` builds a raw surface per
|
||||
// backend, Vulkan's `vkCreateAndroidSurfaceKHR` claims the window
|
||||
// first, and the GLES surface made from the same window then fails
|
||||
// `configure` as lost -- measured here as "In Surface::configure /
|
||||
// Invalid surface" followed by an abort in
|
||||
// `Surface::get_current_texture_view`, "Surface is not configured
|
||||
// for presentation".
|
||||
let mut backends = if cfg!(feature = "force-gles") {
|
||||
Backends::GL
|
||||
} else {
|
||||
Backends::PRIMARY
|
||||
};
|
||||
// No display handle: an Android surface is built from the
|
||||
// `NativeWindow` below, and there is no platform connection to hand
|
||||
// wgpu here the way there is on Wayland.
|
||||
let mut instance = Instance::new(InstanceDescriptor {
|
||||
backends,
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
});
|
||||
// A build already pinned to GLES has nowhere to fall back to.
|
||||
if backends != Backends::GL && instance.enumerate_adapters(backends).block_on().is_empty() {
|
||||
log::warn!(
|
||||
"iris renderer: no {backends:?} adapter on this device, falling back to GLES"
|
||||
);
|
||||
backends = Backends::GL;
|
||||
instance = Instance::new(InstanceDescriptor {
|
||||
backends,
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
});
|
||||
}
|
||||
|
||||
// SAFETY: the `NativeWindow` outlives the surface built from it --
|
||||
// android-view drops the old renderer (and this surface with it)
|
||||
// before handing over a new window, in `surface_changed` below.
|
||||
let surface = instance
|
||||
.create_surface(SurfaceTarget::from(AndroidWindowHandle { window }))
|
||||
.map_err(|error| format!("Could not create the android surface: {error}"))?;
|
||||
|
||||
// Every step from here to a live device reports rather than
|
||||
// panics, for the one reason: on the phone these builds run on
|
||||
// there is no `adb`, so an abort's message reaches a tombstone
|
||||
// nobody can read and the launcher simply restarts the app --
|
||||
// which is what a crash loop with no explanation is. The caller
|
||||
// (`android::view::IrisViewPeer::surface_changed`) puts this
|
||||
// string on screen and in the app's own log ring instead.
|
||||
let adapter = instance
|
||||
.request_adapter(&RequestAdapterOptions {
|
||||
power_preference: PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
..Default::default()
|
||||
})
|
||||
.block_on()
|
||||
.map_err(|error| format!("No usable GPU adapter for backends {backends:?}: {error}"))?;
|
||||
|
||||
// Same request as the winit backend's `UiRenderer::new` -- no
|
||||
// binding-array features, see TEXTURES.md's "Recommended shape".
|
||||
// `iris_core::device_limits()` is shared between the two backends;
|
||||
// see its own doc for why it is not simply `Limits::default()`.
|
||||
let (device, queue) = adapter
|
||||
.request_device(&DeviceDescriptor {
|
||||
required_limits: iris_core::device_limits(),
|
||||
..Default::default()
|
||||
})
|
||||
.block_on()
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"The adapter {} ({:?}) refused a device: {error}",
|
||||
adapter.get_info().name,
|
||||
adapter.get_info().backend,
|
||||
)
|
||||
})?;
|
||||
|
||||
// wgpu's default handler for an error raised outside `UiRenderNode::
|
||||
// new`'s own error scopes (i.e. everything past device creation --
|
||||
// an ordinary frame's `update`/`draw`) is `panic!`, unconditionally,
|
||||
// with no caller able to intervene: the same mechanism that aborted
|
||||
// the P0 bench APK once already, just at a different call site. Log
|
||||
// and record instead of letting that default stand -- RUST.md's P0
|
||||
// box, "every wgpu uncaptured error ... it must never panic in
|
||||
// release".
|
||||
let wgpu_errors = iris_core::WgpuErrorLog::default();
|
||||
let wgpu_errors_for_handler = wgpu_errors.clone();
|
||||
device.on_uncaptured_error(std::sync::Arc::new(move |error| {
|
||||
log::error!("iris wgpu uncaptured error: {error}");
|
||||
wgpu_errors_for_handler.record(error);
|
||||
}));
|
||||
|
||||
let info = adapter.get_info();
|
||||
let adapter_name = info.name.clone();
|
||||
let adapter_backend = info.backend;
|
||||
// Either half can be empty -- the emulator's GLES adapter reports
|
||||
// no `driver` and a long `driver_info`, so joining unconditionally
|
||||
// left a leading space in every log line it appears in.
|
||||
let adapter_driver = [info.driver.as_str(), info.driver_info.as_str()]
|
||||
.into_iter()
|
||||
.filter(|part| !part.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
// Say which adapter won, in the same words `default::render` uses,
|
||||
// and at startup rather than only on the Diagnostics page: the
|
||||
// backend alone (logged by `view.rs` when a renderer is built) does
|
||||
// not separate the cases that matter. In this checkout's emulator
|
||||
// `Gl` is the host's real GPU through virgl, and `Gl` under
|
||||
// `EMU_GPU=software` is SwiftShader on the CPU; on a phone `Vulkan`
|
||||
// is the device's own driver. A frame time or a screenshot with no
|
||||
// record of which of those produced it cannot be read, and the
|
||||
// fallback above is silent by design.
|
||||
log::info!(
|
||||
"iris renderer: {adapter_name} ({adapter_backend:?}, {adapter_driver}) on \
|
||||
{backends:?}"
|
||||
);
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
let surface_format = surface_caps
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| f.is_srgb())
|
||||
.unwrap_or(surface_caps.formats[0]);
|
||||
|
||||
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,
|
||||
height,
|
||||
present_mode: PresentMode::AutoVsync,
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
desired_maximum_frame_latency: 2,
|
||||
view_formats: vec![],
|
||||
};
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let encoder = Self::create_encoder(&device);
|
||||
// Physical pixels, matching the swapchain's own `width`/`height`
|
||||
// exactly -- see `android::view::AndroidUiState::content_scale`'s
|
||||
// field comment for why this is no longer divided into a separate
|
||||
// logical space (that stopgap is what made text blurry, RUST.md's
|
||||
// P0 box). `Len::dp` folds the density in at layout time instead,
|
||||
// so nothing here needs to know it at all.
|
||||
let window_size = iris_core::util::Vec2::new(width as f32, height as f32);
|
||||
let ui = match UiRenderNode::new(&device, &queue, &config, window_size) {
|
||||
Ok(ui) => ui,
|
||||
Err(wgpu_error) => return Err(Self::diagnostic(&adapter, &wgpu_error)),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
encoder,
|
||||
ui,
|
||||
adapter_name,
|
||||
adapter_backend,
|
||||
adapter_driver,
|
||||
wgpu_errors,
|
||||
frame_count: 0,
|
||||
content_scale,
|
||||
})
|
||||
}
|
||||
|
||||
/// The adapter identity plus every limit and downlevel flag
|
||||
/// `create_bind_group_layout` validates a storage buffer or texture
|
||||
/// binding against, followed by wgpu's own error text -- everything a
|
||||
/// person reading this off a screenshot needs to tell "this adapter
|
||||
/// lacks X" from "this is a bug in the layout." Named explicitly rather
|
||||
/// than `{limits:?}`/`{flags:?}` wholesale, because `Limits` alone is
|
||||
/// dozens of fields nobody asked for -- these are exactly the ones
|
||||
/// `UiRenderNode::new`'s layouts (`rsc_layout`, `masks_layout`,
|
||||
/// `primitive_layout`) can fail against, per `CreateBindGroupLayoutError`
|
||||
/// (`wgpu-core::binding_model`) and its downlevel-flag checks
|
||||
/// (`wgpu-core::device::resource`, `VERTEX_STORAGE` in particular --
|
||||
/// the one storage buffer here, `move_offsets`, that is visible to the
|
||||
/// vertex stage).
|
||||
fn diagnostic(adapter: &Adapter, wgpu_error: &str) -> String {
|
||||
let info = adapter.get_info();
|
||||
let limits = adapter.limits();
|
||||
let downlevel = adapter.get_downlevel_capabilities();
|
||||
format!(
|
||||
"iris could not start rendering. Copy this text and send it to Iris.\n\n\
|
||||
adapter: {name} ({backend:?}), driver: {driver} {driver_info}\n\
|
||||
limits: max_storage_buffers_per_shader_stage={max_storage_buffers} \
|
||||
max_sampled_textures_per_shader_stage={max_sampled_textures} \
|
||||
max_bind_groups={max_bind_groups} \
|
||||
max_bindings_per_bind_group={max_bindings} \
|
||||
max_storage_buffer_binding_size={max_storage_binding} \
|
||||
min_storage_buffer_offset_alignment={min_storage_align}\n\
|
||||
downlevel flags: {flags:?}\n\n\
|
||||
{wgpu_error}",
|
||||
name = info.name,
|
||||
backend = info.backend,
|
||||
driver = info.driver,
|
||||
driver_info = info.driver_info,
|
||||
max_storage_buffers = limits.max_storage_buffers_per_shader_stage,
|
||||
max_sampled_textures = limits.max_sampled_textures_per_shader_stage,
|
||||
max_bind_groups = limits.max_bind_groups,
|
||||
max_bindings = limits.max_bindings_per_bind_group,
|
||||
max_storage_binding = limits.max_storage_buffer_binding_size,
|
||||
min_storage_align = limits.min_storage_buffer_offset_alignment,
|
||||
flags = downlevel.flags,
|
||||
)
|
||||
}
|
||||
|
||||
/// The Diagnostics page's whole report: adapter identity, font
|
||||
/// resolution, the atlas's own view count, every uncaptured wgpu error
|
||||
/// so far, and the frame report -- RUST.md's P0 box, "a named
|
||||
/// `Diagnostics` control ... adapter info, limits, fonts found, atlas
|
||||
/// format/pages, wgpu errors so far, frame report". One string rather
|
||||
/// than a struct the caller formats, since the only consumer is a
|
||||
/// plain `TextView` with a "copy this and send it to Iris" affordance,
|
||||
/// the same shape `surface_changed`'s crash report already uses
|
||||
/// (UI_RULES.md: a failure -- or here, a state worth reporting --
|
||||
/// carries enough to act on where it's shown).
|
||||
pub fn diagnostics_report(
|
||||
&self,
|
||||
font: &iris_core::FontDiagnostics,
|
||||
frame_report: &str,
|
||||
) -> String {
|
||||
let errors = self.wgpu_errors.snapshot();
|
||||
let errors_text = if errors.is_empty() {
|
||||
"none".to_string()
|
||||
} else {
|
||||
errors.join("\n ")
|
||||
};
|
||||
format!(
|
||||
"iris diagnostics. Copy this text and send it to Iris.\n\n\
|
||||
adapter: {name} ({backend:?}), driver: {driver}\n\
|
||||
content_scale: {content_scale}\n\
|
||||
atlas format: Rgba8Unorm, views live: {views}\n\
|
||||
fonts: {families_found} families found, default={default_family:?} \
|
||||
mono={default_mono_family:?}\n\
|
||||
fonts resolved: regular={regular:?} bold={bold:?} italic={italic:?} \
|
||||
mono={mono:?}\n\
|
||||
icon font: {icons:?}\n\
|
||||
wgpu errors since surface creation:\n {errors_text}\n\n\
|
||||
{frame_report}",
|
||||
name = self.adapter_name,
|
||||
backend = self.adapter_backend,
|
||||
driver = self.adapter_driver,
|
||||
content_scale = self.content_scale,
|
||||
views = self.ui.view_count(),
|
||||
families_found = font.families_found,
|
||||
default_family = font.default_family,
|
||||
default_mono_family = font.default_mono_family,
|
||||
regular = font.regular_resolved,
|
||||
bold = font.bold_resolved,
|
||||
italic = font.italic_resolved,
|
||||
mono = font.mono_resolved,
|
||||
icons = font.icon_family,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_encoder(device: &Device) -> CommandEncoder {
|
||||
device.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns what changed this frame -- see `FrameDiagnostics`'s doc
|
||||
/// comment for why two of its four fields describe the *previous*
|
||||
/// frame rather than this one. `IrisViewPeer::render` logs this for
|
||||
/// the first `DIAGNOSTIC_FRAMES` frames after each `surface_changed`,
|
||||
/// per RUST.md's P0 box ("the first input frame" investigation): the
|
||||
/// glyph-wipe Iris reported happens on the first tap or scroll after a
|
||||
/// fresh surface, so that is exactly the window a report needs to
|
||||
/// cover, not an arbitrary slice of a long session.
|
||||
pub fn update(&mut self, ui: &mut UiData, render: &mut UiRenderState) -> FrameDiagnostics {
|
||||
let atlas_pages_grown_prev = self.ui.take_atlas_pages_grown();
|
||||
let image_bind_group_creates_prev = self.ui.take_image_bind_group_creates();
|
||||
let stats = self.ui.update(&self.device, &self.queue, ui, render);
|
||||
self.frame_count += 1;
|
||||
FrameDiagnostics {
|
||||
masks_resized: stats.masks_resized,
|
||||
moves_resized: stats.moves_resized,
|
||||
atlas_pages_grown_prev,
|
||||
image_bind_group_creates_prev,
|
||||
}
|
||||
}
|
||||
|
||||
/// Frames drawn on this surface so far -- see `frame_count`'s field
|
||||
/// comment.
|
||||
pub fn frame_count(&self) -> u64 {
|
||||
self.frame_count
|
||||
}
|
||||
|
||||
/// Draws and presents one frame, returning the time spent in
|
||||
/// `queue.submit` plus `present()` -- wherever a driver/GPU/compositor
|
||||
/// wait would actually show up. The caller (`android::view::render`)
|
||||
/// already times the whole frame from its own `redraw_to_submit` start;
|
||||
/// subtracting this from that total is `redraw_to_submit` itself
|
||||
/// (layout, text, primitive building, and this method's own render-pass
|
||||
/// recording). RUST.md's I5 "Where iris's frame time goes" diagnosis,
|
||||
/// added 2026-09-05 -- see `iris_core::FrameReport::record_split`'s own
|
||||
/// doc for the caveat this shares: `present()` is not fenced against
|
||||
/// the GPU actually finishing, so this is "how long the CPU was blocked
|
||||
/// handing the frame off", not confirmed GPU time.
|
||||
pub fn draw(&mut self) -> Duration {
|
||||
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.
|
||||
other => panic!("no surface texture to draw into: {other:?}"),
|
||||
};
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&TextureViewDescriptor::default());
|
||||
|
||||
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
||||
{
|
||||
let render_pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(CLEAR_COLOR),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
self.ui.draw(render_pass);
|
||||
}
|
||||
|
||||
let submit_start = Instant::now();
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.queue.present(output);
|
||||
submit_start.elapsed()
|
||||
}
|
||||
|
||||
/// Physical pixels -- the unit layout and hit-testing use, matching
|
||||
/// the window uniform's own units. See
|
||||
/// `android::view::AndroidUiState::content_scale`'s field comment.
|
||||
pub fn size(&self) -> iris_core::util::Vec2 {
|
||||
iris_core::util::Vec2::new(self.config.width as f32, self.config.height as f32)
|
||||
}
|
||||
|
||||
/// Reconfigures the surface and rewrites the window uniform for a new
|
||||
/// physical size -- deliberately the *only* two things this does.
|
||||
/// `device`, `ui`'s atlas, buffers and bind groups are untouched, so a
|
||||
/// call here (as opposed to a fresh `AndroidRenderer::new`) never
|
||||
/// invalidates a glyph the CPU-side cache already placed in the atlas.
|
||||
/// See `android::view::IrisViewPeer::surface_changed`'s doc comment for
|
||||
/// why that distinction matters -- it is what keeps text on screen
|
||||
/// across an IME resize.
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
self.config.width = width;
|
||||
self.config.height = height;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
let size = iris_core::util::Vec2::new(width as f32, height as f32);
|
||||
self.ui.resize(size, &self.queue);
|
||||
}
|
||||
}
|
||||
|
||||
/// `Tasks`' redraw handle on Android: a background task finishes on the
|
||||
/// tokio thread `Tasks::init` spawned, which is not attached to the JVM, so
|
||||
/// asking for a frame means attaching first. The global ref is what
|
||||
/// survives past the JNI call that handed the `View` to us.
|
||||
///
|
||||
/// **Goes through `View::post_delayed`, not `post_frame_callback`
|
||||
/// directly** -- found the hard way (RUST.md's I5 Android integration):
|
||||
/// `post_frame_callback`'s Java side calls `Choreographer.getInstance()`,
|
||||
/// which throws `IllegalStateException` unless the *calling* thread already
|
||||
/// has a `Looper` (`Choreographer.getInstance()`'s own contract). A tokio
|
||||
/// worker thread, even freshly attached to the JVM, has none -- the crash
|
||||
/// was a `JavaException` inside `View::post_frame_callback`'s `.unwrap()`,
|
||||
/// aborting the process on the second `redraw.request_redraw()` any
|
||||
/// android transcript-screen fetch made. `View.postDelayed(Runnable, 0)`
|
||||
/// is the ordinary Android answer to "queue work onto a View's own UI
|
||||
/// thread from any thread" and needs no Looper of its own; `delayed_callback`
|
||||
/// below is what that Runnable resolves to on the UI thread, where a real
|
||||
/// `post_frame_callback` is safe again.
|
||||
pub struct AndroidRedrawHandle {
|
||||
vm: JavaVM,
|
||||
view: GlobalRef,
|
||||
}
|
||||
|
||||
impl AndroidRedrawHandle {
|
||||
pub fn new(vm: JavaVM, view: GlobalRef) -> Self {
|
||||
Self { vm, view }
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestRedraw for AndroidRedrawHandle {
|
||||
fn request_redraw(&self) {
|
||||
let Ok(mut env) = self.vm.attach_current_thread() else {
|
||||
return;
|
||||
};
|
||||
let local = env.new_local_ref(&self.view).unwrap();
|
||||
View(local).post_delayed(&mut env, 0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user