Files
ai-app/iris/src/default/render.rs
T
irisandClaude Fable 5.1 d8e6bc6e9b iris: bundle Noto Sans for text rendering, apply density scale on both backends
Bundles Noto Sans/Noto Sans Mono (regular/bold/italic/bold-italic, OFL
licensed) into iris-core and registers them ahead of the platform's own
fonts in the SansSerif/Monospace generic-family fallback lists, so text
no longer depends on the platform's font enumeration succeeding or
resolving weight/style correctly. Iris's phone report showed bold spans
rendering as blank gaps of the correct advance width -- the glyph simply
wasn't rasterised -- while the emulator's system fonts happened to
resolve every style; a bundled static-per-style family removes that
platform-dependent step entirely. TextData::font_diagnostics() reports
what was found/resolved, for the startup log and the Diagnostics page.

Also applies a content/device-pixel scale that neither backend had
before: UiRenderNode::new/resize now take the window size explicitly
(logical units) rather than deriving it from the surface's physical
config, so a 16.0 font size is 16 logical units rather than 16 raw
device pixels. Wired on desktop via window.scale_factor() (input events,
window_size, and the render node's own seed); the Android side (density
via DisplayMetrics, touch coordinates, layout root size) is the next
commit.

Also adds WgpuErrorLog and a per-frame atlas-grow counter
(GpuTextures::take_pages_grown), both plumbing for the Android
diagnostics page in the next commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 23:36:29 -04:00

185 lines
7.0 KiB
Rust

use crate::task::RequestRedraw;
use iris_core::{UiData, UiRenderNode, UiRenderState, util::Vec2};
use pollster::FutureExt;
use std::sync::Arc;
use wgpu::*;
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>,
device: Device,
queue: Queue,
config: SurfaceConfiguration,
encoder: CommandEncoder,
pub ui: UiRenderNode,
}
impl UiRenderer {
pub fn update(&mut self, ui: &mut UiData, render: &mut UiRenderState) {
self.ui.update(&self.device, &self.queue, ui, render);
}
pub fn draw(&mut self) {
let output = self.surface.get_current_texture().unwrap();
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);
}
self.queue.submit(std::iter::once(encoder.finish()));
// 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();
output.present();
}
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);
// Logical, matching `new`'s own seed -- see the comment there.
let scale_factor = self.window.scale_factor() as f32;
let logical = Vec2::new(
size.width as f32 / scale_factor,
size.height as f32 / scale_factor,
);
self.ui.resize(logical, &self.queue);
}
fn create_encoder(device: &Device) -> CommandEncoder {
device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("Render Encoder"),
})
}
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
let instance = Instance::new(&InstanceDescriptor {
backends: Backends::PRIMARY,
..Default::default()
});
let surface = instance
.create_surface(window.clone())
.expect("Could not create window surface!");
let adapter = instance
.request_adapter(&RequestAdapterOptions {
power_preference: PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.block_on()
.expect("Could not get adapter!");
// 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_limits: iris_core::device_limits(),
..Default::default()
})
.block_on()
.expect("Could not get device!");
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,
width: size.width,
height: size.height,
// 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![],
};
surface.configure(&device, &config);
let encoder = Self::create_encoder(&device);
// 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).
// Logical size (physical / `scale_factor`), matching what the
// Android backend now reports too (`android::render::
// AndroidRenderer::new`, `content_scale`) -- the swapchain still
// configures at the real physical resolution above; only the
// window uniform layout/hit-testing agree on is scaled. Without
// this a window on any monitor whose scale factor isn't 1.0 would
// have the identical "everything too small" bug RUST.md's P0 box
// found on Iris's phone, just never noticed here because this
// crate's own dev monitors happen to run at 1.0.
let scale_factor = window.scale_factor() as f32;
let logical_size = Vec2::new(size.width as f32 / scale_factor, size.height as f32 / scale_factor);
let ui = UiRenderNode::new(&device, &queue, &config, logical_size)
.expect("Could not create iris render node!");
Self {
surface,
device,
queue,
config,
encoder,
ui,
window,
}
}
pub fn window(&self) -> &Window {
self.window.as_ref()
}
}