Compare commits

..
Author SHA1 Message Date
irisandClaude Opus 5 db65a413c7 Take the machine out of the comments
The draw-cost notes described the machine they were measured on, which is not
something this repository can check or keep true. What is left is what the
file measures and how to read it; the rest lives with the machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 19:04:42 -04:00
irisandClaude Opus 5 0a5eb2d984 Build on wgpu 30
Two majors, and the renderer is under everything else that is left to
extract, so it goes before the slices that would otherwise be written twice.
`image` 0.25.6 -> 0.25.10 rides along; `winit` stays on 0.30.12, since 0.31
is only a prerelease.

What the API asked for:

- An instance now takes the display it will present on, and GLES on Wayland
  needs it -- so the window the surface is made from is handed over with it.
- `get_current_texture` returns a `CurrentSurfaceTexture` rather than a
  `Result`, which replaces an `unwrap` that would have panicked on a resize
  or an occluded window: reconfigure when the surface is outdated, lost or
  suboptimal, and skip the frame when there is nothing to draw into.
- Presenting moved to the queue, after `pre_present_notify` as before.
- Bind group layouts and vertex buffer layouts are sparse now, so a pipeline
  states `Some(layout)` per slot.

Verified with the three render checks and `tests/draw_cost.rs`: identical
pictures, and 33.6/167/587/2855 us per frame at 8/64/256/1024 layers against
33.3/161/588/2903 on wgpu 28.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 18:57:52 -04:00
11 changed files with 30 additions and 384 deletions

No files matched your search

-4
View File
@@ -34,10 +34,6 @@ impl UiRenderState {
self.resized = true; self.resized = true;
} }
pub fn output_size(&self) -> Vec2 {
self.output_size
}
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) { pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
// safety mechanism for memory leaks; might wanna return a result instead so user can // safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not // decide whether to panic or not
+7 -9
View File
@@ -1,6 +1,10 @@
use crate::prelude::*; use crate::prelude::*;
use arboard::Clipboard; use arboard::Clipboard;
use std::{marker::PhantomData, sync::Arc, time::Instant}; use std::{
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use winit::{ use winit::{
event::{Ime, WindowEvent}, event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoopProxy},
@@ -60,12 +64,6 @@ impl DefaultUiState {
} }
} }
impl WakeTaskQueue for Window {
fn wake(&self) {
self.request_redraw();
}
}
pub trait HasDefaultUiState: Sized + 'static { pub trait HasDefaultUiState: Sized + 'static {
fn default_state(&self) -> &DefaultUiState; fn default_state(&self) -> &DefaultUiState;
fn default_state_mut(&mut self) -> &mut DefaultUiState; fn default_state_mut(&mut self) -> &mut DefaultUiState;
@@ -107,8 +105,8 @@ pub struct DefaultRsc<State: 'static> {
} }
impl<State> DefaultRsc<State> { impl<State> DefaultRsc<State> {
pub fn init(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) { fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, recv) = Tasks::init(wake); let (tasks, recv) = Tasks::init(window);
( (
Self { Self {
ui: Default::default(), ui: Default::default(),
+10 -63
View File
@@ -13,13 +13,7 @@ use tokio::{
unbounded_channel as async_channel, unbounded_channel as async_channel,
}, },
}; };
use winit::window::Window;
/// Wakes the host so it applies queued task updates. A task reaches the
/// application only through [`TaskCtx::update`], so this is all the queue
/// needs of a platform.
pub trait WakeTaskQueue: Send + Sync + 'static {
fn wake(&self);
}
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>; pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>; pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
@@ -29,32 +23,29 @@ impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc>
pub struct Tasks<Rsc: HasState> { pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>, start: AsyncSender<BoxTask>,
wake: Arc<dyn WakeTaskQueue>, window: Arc<Window>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>, msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
} }
pub struct TaskCtx<Rsc: HasState> { pub struct TaskCtx<Rsc: HasState> {
send: TaskMsgSender<Rsc>, send: TaskMsgSender<Rsc>,
wake: Arc<dyn WakeTaskQueue>,
} }
impl<Rsc: HasState> TaskCtx<Rsc> { impl<Rsc: HasState> TaskCtx<Rsc> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) { pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
if self.send.send(Box::new(f)).is_ok() { let _ = self.send.send(Box::new(f));
self.wake.wake();
}
} }
} }
impl<Rsc: HasState + 'static> TaskCtx<Rsc> { impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
fn new(send: TaskMsgSender<Rsc>, wake: Arc<dyn WakeTaskQueue>) -> Self { fn new(send: TaskMsgSender<Rsc>) -> Self {
Self { send, wake } Self { send }
} }
} }
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>; type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> { impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Rsc>) { pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
let (start, start_recv) = async_channel(); let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel(); let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| { std::thread::spawn(|| {
@@ -65,7 +56,7 @@ impl<Rsc: HasState> Tasks<Rsc> {
Self { Self {
start, start,
msg_send: msgs, msg_send: msgs,
wake, window,
}, },
msgs_recv, msgs_recv,
) )
@@ -76,9 +67,10 @@ impl<Rsc: HasState> Tasks<Rsc> {
F::CallOnceFuture: Send, F::CallOnceFuture: Send,
{ {
let send = self.msg_send.clone(); let send = self.msg_send.clone();
let wake = self.wake.clone(); let window = self.window.clone();
let _ = self.start.send(Box::pin(async move { let _ = self.start.send(Box::pin(async move {
task(TaskCtx::new(send, wake)).await; task(TaskCtx::new(send)).await;
window.request_redraw();
})); }));
} }
} }
@@ -88,48 +80,3 @@ async fn listen(mut recv: AsyncReceiver<BoxTask>) {
tokio::spawn(task); tokio::spawn(task);
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use std::{sync::mpsc::sync_channel, time::Duration};
struct TestRsc;
impl HasState for TestRsc {
type State = usize;
}
/// Signals rather than counts, so the test waits for a wake instead of
/// racing the task thread to sample it.
struct WakeSignal(std::sync::mpsc::SyncSender<()>);
impl WakeTaskQueue for WakeSignal {
fn wake(&self) {
let _ = self.0.send(());
}
}
#[test]
fn every_update_wakes_the_host() {
let (woken, wakes) = sync_channel(8);
let (mut tasks, updates) = Tasks::<TestRsc>::init(Arc::new(WakeSignal(woken)));
tasks.spawn(async move |mut ctx| {
ctx.update(|state: &mut usize, _| *state += 1);
ctx.update(|state: &mut usize, _| *state += 2);
});
let second = Duration::from_secs(1);
let (mut state, mut rsc) = (0, TestRsc);
for _ in 0..2 {
wakes.recv_timeout(second).expect("no wake for an update");
updates.recv_timeout(second).unwrap()(&mut state, &mut rsc);
}
assert_eq!(state, 3);
assert!(
wakes.recv_timeout(Duration::from_millis(100)).is_err(),
"woken with nothing to apply"
);
}
}
-154
View File
@@ -1,154 +0,0 @@
//! A ui with no window: build a tree, run frames, move a pointer, and read
//! back where widgets landed.
//!
//! It does not draw. A claim about pixels still needs a real surface.
use crate::prelude::*;
use std::{sync::Arc, time::Duration};
/// The harness drains the update queue itself, so there is no loop to wake.
struct NoWake;
impl WakeTaskQueue for NoWake {
fn wake(&self) {}
}
#[derive(Default)]
pub struct HarnessState {
pub root: Option<StrongWidget>,
}
impl HasRoot for HarnessState {
fn set_root(&mut self, root: StrongWidget) {
self.root = Some(root);
}
}
pub struct Harness {
pub rsc: DefaultRsc<HarnessState>,
pub render: UiRenderState,
pub state: HarnessState,
updates: TaskMsgReceiver<DefaultRsc<HarnessState>>,
cursor: CursorState,
}
impl Harness {
/// `size` is the output in physical pixels.
pub fn new(size: impl Into<Vec2>) -> Self {
let (rsc, updates) = DefaultRsc::init(Arc::new(NoWake));
let mut render = UiRenderState::new();
render.resize(size);
Self {
rsc,
render,
state: HarnessState::default(),
updates,
cursor: CursorState::default(),
}
}
pub fn size(&self) -> Vec2 {
self.render.output_size()
}
pub fn resize(&mut self, size: impl Into<Vec2>) {
self.render.resize(size);
}
/// Sets the root and lays it out, so a pointer event has something to hit.
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
widget.set_root(&mut self.rsc, &mut self.state);
self.frame();
}
pub fn needs_redraw(&self) -> bool {
self.render
.needs_redraw(&self.state.root, self.rsc.widgets())
}
pub fn apply_updates(&mut self) -> usize {
let mut applied = 0;
while let Ok(update) = self.updates.try_recv() {
update(&mut self.state, &mut self.rsc);
applied += 1;
}
applied
}
/// Waits for a task's first update, then applies everything waiting.
/// False if none arrived in time.
#[must_use]
pub fn await_update(&mut self, timeout: Duration) -> bool {
let Ok(update) = self.updates.recv_timeout(timeout) else {
return false;
};
update(&mut self.state, &mut self.rsc);
self.apply_updates();
true
}
/// Lays the tree out and builds its primitives.
pub fn frame(&mut self) {
self.apply_updates();
self.render.update(&self.state.root, &mut self.rsc);
}
/// Where the last frame put a widget, or `None` if it drew nothing.
pub fn region(&self, id: &impl IdLike) -> Option<PixelRegion> {
self.render.window_region(id)
}
pub fn move_to(&mut self, pos: impl Into<Vec2>) {
self.cursor.pos = pos.into();
self.cursor.exists = true;
self.sense();
}
pub fn leave(&mut self) {
self.cursor.exists = false;
self.sense();
}
pub fn press(&mut self, button: CursorButton) {
self.button(button).update(true);
self.sense();
}
pub fn release(&mut self, button: CursorButton) {
self.button(button).update(false);
self.sense();
}
/// A wheel carries no position, so this goes wherever
/// [`move_to`](Self::move_to) last put the cursor -- nowhere, until it has
/// been called.
pub fn scroll(&mut self, delta: impl Into<Vec2>) {
self.cursor.scroll_delta = delta.into();
self.sense();
}
pub fn click(&mut self, pos: impl Into<Vec2>) {
self.move_to(pos);
self.press(CursorButton::Left);
self.release(CursorButton::Left);
}
fn button(&mut self, button: CursorButton) -> &mut ActivationState {
let buttons = &mut self.cursor.buttons;
match button {
CursorButton::Left => &mut buttons.left,
CursorButton::Middle => &mut buttons.middle,
CursorButton::Right => &mut buttons.right,
}
}
/// Dispatches against the last [`frame`](Self::frame)'s layout, which is
/// what a window delivers input against too.
fn sense(&mut self) {
let cursor = self.cursor.clone();
let size = self.render.output_size();
self.render
.run_sensors(&mut self.rsc, &mut self.state, cursor, size);
self.cursor.end_frame();
}
}
-1
View File
@@ -7,7 +7,6 @@
pub mod default; pub mod default;
pub mod event; pub mod event;
pub mod harness;
pub mod widget; pub mod widget;
pub use iris_core as core; pub use iris_core as core;
+2 -2
View File
@@ -4,7 +4,7 @@ mod max_size;
mod offset; mod offset;
mod pad; mod pad;
mod scroll; mod scroll;
mod set_size; mod sized;
mod span; mod span;
mod stack; mod stack;
@@ -14,6 +14,6 @@ pub use max_size::*;
pub use offset::*; pub use offset::*;
pub use pad::*; pub use pad::*;
pub use scroll::*; pub use scroll::*;
pub use set_size::*; pub use sized::*;
pub use span::*; pub use span::*;
pub use stack::*; pub use stack::*;
@@ -1,12 +1,12 @@
use crate::prelude::*; use crate::prelude::*;
pub struct SetSize { pub struct Sized {
pub inner: StrongWidget, pub inner: StrongWidget,
pub x: Option<Len>, pub x: Option<Len>,
pub y: Option<Len>, pub y: Option<Len>,
} }
impl SetSize { impl Sized {
fn apply_to_outer(&self, ctx: &mut SizeCtx) { fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x { if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest()); ctx.outer.x.select_len(x.apply_rest());
@@ -17,7 +17,7 @@ impl SetSize {
} }
} }
impl Widget for SetSize { impl Widget for Sized {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner); painter.widget(&self.inner);
} }
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::Unsize; use std::marker::{Sized, Unsize};
pub struct WidgetPtr { pub struct WidgetPtr {
pub inner: Option<StrongWidget>, pub inner: Option<StrongWidget>,
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::PhantomData; use std::marker::{PhantomData, Sized};
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> { pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
pub content: String, pub content: String,
+6 -6
View File
@@ -31,9 +31,9 @@ widget_trait! {
} }
} }
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> { fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> {
let size = size.into(); let size = size.into();
move |state| SetSize { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
x: Some(size.x), x: Some(size.x),
y: Some(size.y), y: Some(size.y),
@@ -58,18 +58,18 @@ widget_trait! {
} }
} }
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> { fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into(); let len = len.into();
move |state| SetSize { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
x: Some(len), x: Some(len),
y: None, y: None,
} }
} }
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> { fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into(); let len = len.into();
move |state| SetSize { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
x: None, x: None,
y: Some(len), y: Some(len),
-140
View File
@@ -1,140 +0,0 @@
//! Layout, hit testing and task updates, driven without a window.
use std::{cell::RefCell, rc::Rc, time::Duration};
use iris::harness::Harness;
use iris::prelude::*;
/// `PixelRegion` neither compares nor prints.
fn corners(h: &Harness, id: &impl IdLike) -> (f32, f32, f32, f32) {
let region = h.region(id).expect("widget drew nothing");
(
region.top_left.x,
region.top_left.y,
region.bot_right.x,
region.bot_right.y,
)
}
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
let left = rect(Color::RED).width(100).add(&mut h.rsc);
let right = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
(left.id(), right.id())
}
#[test]
fn a_span_gives_each_child_the_width_it_asked_for() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
assert_eq!(corners(&h, &left), (0.0, 0.0, 100.0, 200.0));
assert_eq!(corners(&h, &right), (100.0, 0.0, 400.0, 200.0));
}
#[test]
fn resizing_relays_out_against_the_new_output() {
let mut h = Harness::new((400, 200));
let (left, right) = two_rects(&mut h);
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_eq!(corners(&h, &left), (0.0, 0.0, 100.0, 100.0));
assert_eq!(corners(&h, &right), (100.0, 0.0, 800.0, 100.0));
}
#[test]
fn a_press_reaches_only_the_widget_under_the_cursor() {
let mut h = Harness::new((400, 200));
let clicks = Rc::new(RefCell::new(Vec::new()));
let (on_left, on_right) = (clicks.clone(), clicks.clone());
let left = rect(Color::RED)
.width(100)
.on(CursorSense::click(), move |_, _| {
on_left.borrow_mut().push("left")
})
.add(&mut h.rsc);
let right = rect(Color::BLUE)
.on(CursorSense::click(), move |_, _| {
on_right.borrow_mut().push("right")
})
.add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
h.click((50, 100));
assert_eq!(*clicks.borrow(), ["left"]);
h.click((300, 100));
assert_eq!(*clicks.borrow(), ["left", "right"]);
}
#[test]
fn hover_ends_when_the_cursor_leaves_the_window() {
let mut h = Harness::new((400, 200));
let hovered = Rc::new(RefCell::new(0));
let ended = Rc::new(RefCell::new(0));
let (h_count, e_count) = (hovered.clone(), ended.clone());
let widget = rect(Color::RED)
.on(CursorSense::HoverStart, move |_, _| {
*h_count.borrow_mut() += 1
})
.on(CursorSense::HoverEnd, move |_, _| {
*e_count.borrow_mut() += 1
})
.add(&mut h.rsc);
h.set_root(widget);
h.move_to((200, 100));
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
// A second sample inside the same widget is not a second hover.
h.move_to((210, 100));
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
h.leave();
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1));
}
#[test]
fn a_task_update_reaches_the_tree() {
let mut h = Harness::new((400, 200));
let widget = rect(Color::RED).add(&mut h.rsc);
h.set_root(widget.task_on(CursorSense::click(), async move |mut ctx| {
ctx.update(move |_, rsc| widget(rsc).color = Color::BLUE);
}));
h.click((200, 100));
assert!(
h.await_update(Duration::from_secs(5)),
"the task sent no update"
);
assert_eq!(h.rsc[widget].color, Color::BLUE);
}
#[test]
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
let mut h = Harness::new((400, 200));
// Twice the window's height, so there is 200 to scroll.
let top = rect(Color::RED).height(200).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
h.move_to((200, 100));
// `Scroll` starts snapped to the end.
assert_eq!(corners(&h, &top).1, -200.0);
// The handler scales a wheel line by 50.
h.scroll((0, 1));
h.frame();
assert_eq!(corners(&h, &top).1, -150.0);
h.scroll((0, 10));
h.frame();
assert_eq!(corners(&h, &top).1, 0.0);
}