5 Commits
Author SHA1 Message Date
irisandClaude Opus 5 e0ee7d6e94 RUST.md: record E0 and I0b, with what they measured
E0 is done (NDK r29, cargo-ndk 4.1.2, verified by cross-compiling to both
ABIs) and I0b is done. Corrects this file's guess at why iris would not
build, notes the const-traits family as the gates to re-read whenever the
pin is advanced, and records the cold build weight against the "slow in
debug" worry: 43s and 2.1 GB plain, 1m46s and 1.5 GB with dependencies at
opt-level 2.

Also records an open defect found on the way -- iris sometimes keeps its
pre-configure window size for good -- with what was ruled out, since it
is timing-sensitive enough that any added print hides it, and I2 will
meet it on every rotation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:47:44 -04:00
irisandClaude Opus 5 b6b0928087 Take winit out of iris-core, which makes it build for Android
iris-core wanted exactly one thing from winit: PhysicalSize<u32> in
UiRenderNode::resize's signature, for two numbers it immediately turned
into floats. That pulled a whole windowing backend into the layer below
it. `resize` takes `impl Into<Vec2>` now, matching UiRenderState::resize
beside it.

The consequence is the reason: with winit in the graph, an Android build
of the core failed in android-activity, which needs a backend feature
nothing here selects and which iris should not be going through at all --
the plan is android-view. Without it, `cargo ndk -t arm64-v8a -P 26 build
-p iris-core` produces an rlib in 30s with wgpu's Android backend
included. So the widget, layout and render core already builds for the
phone, and what remains is the surface, the input and the IME.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:47:44 -04:00
irisandClaude Opus 5 12221ea025 Make iris ask for the frame a resize needs
`update` redrew everything when `resized` was set, but `needs_redraw` --
which is what decides whether to request a frame at all -- did not know
about `resized`. A condition in one and not the other is a frame nobody
asks for and a stale window. The two share one `needs_redraw_all` now.

Latent on Wayland, because winit requests a redraw after a resize by
itself; a resize changes neither the root nor any widget, so nothing else
here would have asked. It stops being latent on Android, where the
surface work will not have winit underneath it and every rotation and
keyboard open is a resize.

This is not a fix for the startup defect recorded in RUST.md, where the
window keeps its pre-configure layout: that reproduces with this change
in place, and the frame it needs is requested and drawn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:47:44 -04:00
irisandClaude Opus 5 5e23c8b0c0 Add a headless runner for iris examples
This VM has no display but does have a real GPU -- Vulkan 1.4 through
Venus and GL 4.6 through virgl, onto the host's card -- so the only thing
missing for a winit window is a compositor. Same trick `emu` uses for the
Android emulator: a headless sway, with grim for the picture.

It starts its own compositor rather than joining `emu`'s. sway tiles, so
adding a window to the one an emulator sits in resizes that emulator, and
a peer session's `emu up` could join at any moment. Xwayland is off here
because winit speaks Wayland; `emu` forces it on only because the Android
emulator's renderer speaks GLX.

It waits for the window to be mapped rather than sleeping a fixed time:
the first version's fixed sleep captured an all-black screen when sway
had started in the same invocation, which is indistinguishable from an
app that draws nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:47:27 -04:00
irisandClaude Opus 5 caaa733caa Make iris build: pin a dated nightly and migrate const-trait impls
The vendored January tree did not parse at all on a current nightly: 36
errors in iris-core, all from one syntax change. `impl const Trait for T`
is now `const impl Trait for T`, with generics on the `impl`. Bounds are
unaffected, and the traits were already declared `const trait` -- so the
diagnosis recorded in RUST.md was wrong, and pinning back to a January
nightly would only have deferred this. Everything else (the unresolved
UiVec2/Vec2/impl_op imports, a Color<u8> resolving to wgpu_types::Color)
cascaded from the seven files that failed to parse.

The pin is dated rather than `nightly` because that is exactly the
failure: a rolling channel moving under a build Dev Updater runs
unattended. It carries the components and Android targets too, so a
fresh clone provisions itself.

Also drops two `#![feature]` gates the compiler reports as declared and
unused, since the build stays warning-clean, and takes rustfmt's import
order in attr.rs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:47:27 -04:00
18 changed files with 299 additions and 57 deletions

No files matched your search

+119 -23
View File
@@ -418,9 +418,20 @@ Android backend wants API 26 (a libc symbol). The real phone has Vulkan.
Per the standing rule, a rig limit is something to fix before it is Per the standing rule, a rig limit is something to fix before it is
accepted. accepted.
- [ ] **E0 — toolchain.** No NDK is installed (`~/Android/Sdk/ndk` is - [x] **E0 — toolchain (done 2026-09-04).** Installed under the
empty; the Android Rust targets are). Install the NDK under the user-owned SDK: **NDK r29 (`29.0.14206865`)**, 2.4 GB at
user-owned SDK, `cargo-ndk`, and record the versions here. `~/Android/Sdk/ndk/29.0.14206865`, the newest stable — r30 is still
at rc.3. **cargo-ndk 4.1.2**. Verified by cross-compiling a scratch
`cdylib` to both ABIs: `file` reports "for Android 26, built by NDK
r29 (14206865)" for `aarch64-linux-android` and
`x86_64-linux-android`. Two things to know at the call site.
**cargo-ndk 4's API-level flag is `-P`, not `-p`**`-p` is now
passed through to cargo as `--package`, so the old
`cargo ndk -t arm64-v8a -p 26` panics with `unknown package: 26`
*and dumps the whole environment to stdout* as a bug report, which is
worth not doing in a log somebody might paste. And the Android
targets were installed for **stable** only; the pinned nightly needs
its own, which `iris/rust-toolchain.toml` now declares.
- [ ] **E1 — android-view's Masonry demo on this emulator.** Pass: it - [ ] **E1 — android-view's Masonry demo on this emulator.** Pass: it
builds with `cargo-ndk` + Gradle, renders on the GPU, and the phone's builds with `cargo-ndk` + Gradle, renders on the GPU, and the phone's
own keyboard types into its editor with autocorrect and suggestions. own keyboard types into its editor with autocorrect and suggestions.
@@ -461,26 +472,111 @@ step measured.
`7b54aaf` ("readme", 2026-01-29), byte-identical to the public `7b54aaf` ("readme", 2026-01-29), byte-identical to the public
GitHub copy, so a later reconciliation has a known base. A crate GitHub copy, so a later reconciliation has a known base. A crate
that uses it says `iris = { path = "../iris" }`. that uses it says `iris = { path = "../iris" }`.
- [ ] **I0b — make it build here.** Pin a dated nightly in - [x] **I0b — make it build here (done 2026-09-04).** iris now builds,
`iris/rust-toolchain.toml` (the machine has clippy-clean and rustfmt-clean at the defaults, on a pinned dated
`nightly-x86_64-unknown-linux-gnu`, rustc 1.100.0-nightly nightly, and the `tabs` example draws on this VM's GPU.
2026-08-25); list every `#![feature]` gate with what it is for, so
each can be retired when it stabilises. Get `cargo build`, `clippy` **The pin** is `nightly-2026-09-03` (rustc 1.100.0-nightly,
and `fmt` clean at the defaults and the `tabs` example running in a `2e2b193f8`), declared in `iris/rust-toolchain.toml` along with the
window (this VM has no display: a headless compositor is what `clippy`/`rustfmt` components and the two Android targets, so a
`emu` uses for the emulator, and the same trick serves here). fresh clone provisions itself. It is dated rather than `nightly`
Pass: a fresh clone builds unattended from the pinned toolchain. because the whole failure below was a rolling channel moving under
**Measured 2026-09-04**: `cargo +nightly check` on that nightly an unattended build. Installed with `--profile minimal`: 912 MB.
fails in `iris-core` with 36 errors, all one cause — the
`const_trait_impl` feature changed shape between January and **The 36 errors were one syntax change, and the earlier diagnosis in
August: `impl const Trait for T` is now rejected with "expected a this file was wrong.** It is not that a trait must now be declared
trait, found type" unless the trait itself is declared `const trait` — the vendored tree already declares them that way,
`const trait`, at seven sites (`num.rs`, `align.rs`, `axis.rs`, which is how it was written in January. What changed is the *impl*
`pos.rs`, `color.rs`, `math.rs`, `vec2.rs`), and the unresolved keyword order: `impl const Trait for T` is now
`UiVec2`/`Vec2`/`impl_op` imports cascade from those. That is the `const impl Trait for T`, and generics go on the `impl`
nightly-drift risk in one build; the fix is mechanical (declare (`const impl<T: [const] Foo> Bar for T`). Bounds are unaffected;
the traits `const trait`), and it is the first item of I0b rather `T: const Foo`, `T: [const] Foo` and `impl const Foo` in argument
than pinning back to a January nightly, which would only defer it. position all still compile. Everything else — the unresolved
`UiVec2`/`Vec2`/`impl_op` imports, and a `Color<u8>` that resolved
to `wgpu_types::Color` — cascaded from the seven files that failed
to parse. The rewrite was mechanical across 20 sites and took the
workspace from 36 errors to 0.
**`#![feature]` gates, 12 after this step** (two were declared and
unused, and were removed: `map_try_insert`, `const_cmp`).
Load-bearing and worth watching: `const_trait_impl`, `const_ops`,
`const_convert`, `const_destruct` are the const-traits family and
the one that has already broken once — they move together, so
advancing the pin means re-reading this section. `unboxed_closures`
+ `fn_traits` (postfix builder API) and `unsize` +
`coerce_unsized` (widget handles) are pairs. The rest are
individually small: `macro_metavar_expr_concat`, `portable_simd`,
`associated_type_defaults`, `option_into_flat_iter`, and `gen_blocks`
in the top crate.
**Running it headless.** `iris/run-headless.sh EXAMPLE [--shot PNG]`
with `iris/headless.conf`, the same trick `emu` uses: a headless
sway, and `grim` for the picture. It deliberately starts its *own*
compositor rather than joining `emu`'s — sway tiles, so adding a
window to the one an emulator sits in resizes that emulator.
Unlike `emu`'s it disables Xwayland, since winit speaks Wayland.
**This VM has a real GPU for this**: Vulkan 1.4 through Venus onto
the host's RX 7900 XT, and GL 4.6 through virgl — so desktop wgpu
work here is not software-rasterised, unlike inside the emulator.
**iris has no tests at all** (`cargo test --workspace`: 0 passed
across 6 targets). Nothing to keep passing, and nothing to catch a
regression — worth knowing before I1 changes the text stack.
**`iris-core` no longer depends on winit, and now cross-compiles to
Android.** It wanted exactly one thing from it — `PhysicalSize<u32>`
in `UiRenderNode::resize`'s signature, for two numbers it immediately
turned into floats — and that pulled a whole windowing backend into
the layer below it, the wrong direction. `resize` takes
`impl Into<Vec2>` now, like `UiRenderState::resize` beside it already
did. The consequence is the point: with winit in the graph an Android
build of the core failed in `android-activity` (which needs a backend
feature nothing here selects), and without it
`cargo ndk -t arm64-v8a -P 26 build -p iris-core` finishes in 30s and
produces an rlib, wgpu's Android backend included. So **iris's
widget, layout and render core already builds for the phone**, and
what I2 has to supply is the surface, the input and the IME — not a
port of the library.
**Build weight, cold, on this VM's 8 cores** (`rm -rf target`, then
`cargo build --example tabs`), since "the Linebender stack is slow in
debug" was the worry behind this question: plain debug **43s** and a
2.1 GB `target/`; with the `[profile.dev.package."*"] opt-level = 2`
knob, **1m46s** and 1.5 GB. So iris's own wgpu + winit + cosmic-text
graph is not the slow thing — which makes it a calibration for E1
rather than an answer about Masonry, whose graph adds Vello, Parley,
Fontique and Skrifa. Runtime cost of the knob was not measured here.
**Open defect found while doing this: iris sometimes never adopts
the window's real size.** Measured on the headless rig, ~3 starts in
15: the `tabs` example settles showing its 800×600 startup layout in
the top-left of a 1920×1200 surface, black around it, and stays that
way indefinitely — it is not a screenshot taken too early, since the
picture is byte-identical for the next four seconds. What is *not*
the cause, each checked: the winit event order is identical in good
and bad runs (`Resized(800×600)`, two redraws, `Resized(1920×1200)`,
one redraw), the swapchain reports `1920×1200` and
`suboptimal=false` on that last draw, and `output_size` is
`(1920, 1200)` going into it. It is timing-sensitive in the way that
makes it expensive: adding a single `eprintln!` anywhere in the draw
or event path hides it completely (0 in 16), which is why the
instrumentation above could not catch it in the act. A pointer move
does not repair it, because iris only redraws when something
changed; an output mode change does, because that is another resize.
Left open rather than guessed at. It matters most for **I2**, where
every rotation and every keyboard open is a resize, so a stale frame
would be the normal case rather than a rare one; a Wayland-level
trace of the xdg-surface configure/ack/commit sequence is the next
step, not more `eprintln`.
One thing was fixed on the way, and it is not that bug: `update`
redrew everything when `resized` was set, but `needs_redraw` — which
is what decides whether to *ask* for a frame — did not know about
`resized` at all. The two now share one `needs_redraw_all`, since a
condition in one and not the other is a frame nobody requests. It is
latent on Wayland only because winit asks for a redraw after a resize
by itself; on Android, where the surface work of I2 will not have
winit underneath it, nothing else here would have asked.
- [ ] **I1 — the text stack decision.** iris uses cosmic-text; the - [ ] **I1 — the text stack decision.** iris uses cosmic-text; the
transcript needs rich inline spans (links, code chips, colour), transcript needs rich inline spans (links, code chips, colour),
selection across many widgets with the platform's handles on the selection across many widgets with the platform's handles on the
-1
View File
@@ -1070,7 +1070,6 @@ dependencies = [
"fxhash", "fxhash",
"image", "image",
"wgpu", "wgpu",
"winit",
] ]
[[package]] [[package]]
-1
View File
@@ -4,7 +4,6 @@ version.workspace = true
edition.workspace = true edition.workspace = true
[dependencies] [dependencies]
winit = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
bytemuck ={ workspace = true } bytemuck ={ workspace = true }
image = { workspace = true } image = { workspace = true }
+1 -1
View File
@@ -1,4 +1,4 @@
use crate::{UiRsc, WidgetIdFn, WidgetLike, WeakWidget}; use crate::{UiRsc, WeakWidget, WidgetIdFn, WidgetLike};
pub trait WidgetAttr<Rsc, W: ?Sized> { pub trait WidgetAttr<Rsc, W: ?Sized> {
type Input; type Input;
-2
View File
@@ -2,10 +2,8 @@
#![feature(const_ops)] #![feature(const_ops)]
#![feature(const_trait_impl)] #![feature(const_trait_impl)]
#![feature(const_convert)] #![feature(const_convert)]
#![feature(map_try_insert)]
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
#![feature(const_cmp)]
#![feature(const_destruct)] #![feature(const_destruct)]
#![feature(portable_simd)] #![feature(portable_simd)]
#![feature(associated_type_defaults)] #![feature(associated_type_defaults)]
+5 -5
View File
@@ -5,19 +5,19 @@ pub const trait UiNum {
fn to_f32(self) -> f32; fn to_f32(self) -> f32;
} }
impl const UiNum for f32 { const impl UiNum for f32 {
fn to_f32(self) -> f32 { fn to_f32(self) -> f32 {
self self
} }
} }
impl const UiNum for u32 { const impl UiNum for u32 {
fn to_f32(self) -> f32 { fn to_f32(self) -> f32 {
self as f32 self as f32
} }
} }
impl const UiNum for i32 { const impl UiNum for i32 {
fn to_f32(self) -> f32 { fn to_f32(self) -> f32 {
self as f32 self as f32
} }
@@ -27,7 +27,7 @@ pub const fn vec2(x: impl const UiNum, y: impl const UiNum) -> Vec2 {
Vec2::new(x.to_f32(), y.to_f32()) Vec2::new(x.to_f32(), y.to_f32())
} }
impl<T: const UiNum + Copy> const From<T> for Vec2 { const impl<T: const UiNum + Copy> From<T> for Vec2 {
fn from(v: T) -> Self { fn from(v: T) -> Self {
Self { Self {
x: v.to_f32(), x: v.to_f32(),
@@ -36,7 +36,7 @@ impl<T: const UiNum + Copy> const From<T> for Vec2 {
} }
} }
impl<T: const UiNum, U: const UiNum> const From<(T, U)> for Vec2 const impl<T: const UiNum, U: const UiNum> From<(T, U)> for Vec2
where where
(T, U): const Destruct, (T, U): const Destruct,
{ {
+1 -1
View File
@@ -187,7 +187,7 @@ impl From<CardinalAlign> for Align {
} }
} }
impl const From<RegionAlign> for UiVec2 { const impl From<RegionAlign> for UiVec2 {
fn from(align: RegionAlign) -> Self { fn from(align: RegionAlign) -> Self {
Self::rel(align.rel()) Self::rel(align.rel())
} }
+2 -2
View File
@@ -74,14 +74,14 @@ pub const trait AxisT {
} }
pub struct XAxis; pub struct XAxis;
impl const AxisT for XAxis { const impl AxisT for XAxis {
fn get() -> Axis { fn get() -> Axis {
Axis::X Axis::X
} }
} }
pub struct YAxis; pub struct YAxis;
impl const AxisT for YAxis { const impl AxisT for YAxis {
fn get() -> Axis { fn get() -> Axis {
Axis::Y Axis::Y
} }
+2 -2
View File
@@ -124,13 +124,13 @@ impl Display for UiVec2 {
impl_op!(UiVec2 Add add; x y); impl_op!(UiVec2 Add add; x y);
impl_op!(UiVec2 Sub sub; x y); impl_op!(UiVec2 Sub sub; x y);
impl const From<Vec2> for UiVec2 { const impl From<Vec2> for UiVec2 {
fn from(abs: Vec2) -> Self { fn from(abs: Vec2) -> Self {
Self::abs(abs) Self::abs(abs)
} }
} }
impl<T: const UiNum, U: const UiNum> const From<(T, U)> for UiVec2 const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
where where
(T, U): const Destruct, (T, U): const Destruct,
{ {
+2 -2
View File
@@ -144,7 +144,7 @@ impl ColorNum for f32 {
unsafe impl bytemuck::Pod for Color<u8> {} unsafe impl bytemuck::Pod for Color<u8> {}
impl const F32Conversion for f32 { const impl F32Conversion for f32 {
fn to(self) -> f32 { fn to(self) -> f32 {
self self
} }
@@ -153,7 +153,7 @@ impl const F32Conversion for f32 {
} }
} }
impl const F32Conversion for u8 { const impl F32Conversion for u8 {
fn to(self) -> f32 { fn to(self) -> f32 {
self as f32 / 255.0 self as f32 / 255.0
} }
+9 -5
View File
@@ -3,14 +3,13 @@ use std::num::NonZero;
use crate::{ use crate::{
UiData, UiRenderState, UiData, UiRenderState,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf}, render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
util::HashMap, util::{HashMap, Vec2},
}; };
use data::WindowUniform; use data::WindowUniform;
use wgpu::{ use wgpu::{
util::{BufferInitDescriptor, DeviceExt}, util::{BufferInitDescriptor, DeviceExt},
*, *,
}; };
use winit::dpi::PhysicalSize;
mod data; mod data;
mod primitive; mod primitive;
@@ -118,10 +117,15 @@ impl UiRenderNode {
} }
} }
pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) { /// Takes a size rather than a window type: this is the only thing the
/// core wanted from winit, and depending on a windowing backend for two
/// numbers is what put `android-activity` in the core's graph for an
/// Android build that is meant to go through android-view instead.
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
let size = size.into();
let slice = &[WindowUniform { let slice = &[WindowUniform {
width: size.width as f32, width: size.x,
height: size.height as f32, height: size.y,
}]; }];
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
} }
+14 -2
View File
@@ -52,7 +52,7 @@ impl UiRenderState {
); );
} }
let root = root.into(); let root = root.into();
if self.root_changed(root) || self.resized { if self.needs_redraw_all(root) {
self.redraw_all(root, rsc); self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id()); self.old_root = root.map(|r| r.id());
self.resized = false; self.resized = false;
@@ -218,12 +218,24 @@ impl UiRenderState {
root.into().map(|r| r.id()) != self.old_root root.into().map(|r| r.id()) != self.old_root
} }
/// What `update` will redraw everything for. Named and shared with
/// `needs_redraw` rather than written out twice, because the two must
/// agree: `needs_redraw` is what asks for the frame that `update` would
/// draw, so a condition in one and not the other is a frame nobody
/// requests and a stale window. `resized` was missing from `needs_redraw`,
/// which is latent on Wayland only because winit asks for a redraw after a
/// resize by itself -- a resize changes neither the root nor any widget,
/// so nothing else here would have asked.
fn needs_redraw_all<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
self.root_changed(root) || self.resized
}
pub fn needs_redraw<'a>( pub fn needs_redraw<'a>(
&self, &self,
root: impl Into<Option<&'a StrongWidget>>, root: impl Into<Option<&'a StrongWidget>>,
widgets: &Widgets, widgets: &Widgets,
) -> bool { ) -> bool {
self.root_changed(root) || widgets.has_updates() self.needs_redraw_all(root) || widgets.has_updates()
} }
pub fn active_widgets(&self) -> usize { pub fn active_widgets(&self) -> usize {
+9 -8
View File
@@ -9,15 +9,16 @@ pub const trait DivOr {
fn div_or(self, rhs: Self, other: Self) -> Self; fn div_or(self, rhs: Self, other: Self) -> Self;
} }
impl const DivOr for f32 { const impl DivOr for f32 {
fn div_or(self, rhs: Self, other: Self) -> Self { fn div_or(self, rhs: Self, other: Self) -> Self {
let res = self / rhs; let res = self / rhs;
if res.is_nan() { other } else { res } if res.is_nan() { other } else { res }
} }
} }
impl<T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy> const const impl<
LerpUtil for T T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy,
> LerpUtil for T
{ {
/// linear interpolation /// linear interpolation
/// from * (1.0 - self) + to * self /// from * (1.0 - self) + to * self
@@ -37,7 +38,7 @@ macro_rules! impl_op {
use super::*; use super::*;
#[allow(unused_imports)] #[allow(unused_imports)]
use std::ops::*; use std::ops::*;
impl const $op for $T { const impl $op for $T {
type Output = Self; type Output = Self;
fn $fn(self, rhs: Self) -> Self::Output { fn $fn(self, rhs: Self) -> Self::Output {
@@ -46,12 +47,12 @@ macro_rules! impl_op {
} }
} }
} }
impl const $opa for $T { const impl $opa for $T {
fn $fna(&mut self, rhs: Self) { fn $fna(&mut self, rhs: Self) {
*self = self.$fn(rhs); *self = self.$fn(rhs);
} }
} }
impl const $op<f32> for $T { const impl $op<f32> for $T {
type Output = Self; type Output = Self;
fn $fn(self, rhs: f32) -> Self::Output { fn $fn(self, rhs: f32) -> Self::Output {
@@ -60,7 +61,7 @@ macro_rules! impl_op {
} }
} }
} }
impl const $op<$T> for f32 { const impl $op<$T> for f32 {
type Output = $T; type Output = $T;
fn $fn(self, rhs: $T) -> Self::Output { fn $fn(self, rhs: $T) -> Self::Output {
@@ -69,7 +70,7 @@ macro_rules! impl_op {
} }
} }
} }
impl const $opa<f32> for $T { const impl $opa<f32> for $T {
fn $fna(&mut self, rhs: f32) { fn $fna(&mut self, rhs: f32) {
*self = self.$fn(rhs); *self = self.$fn(rhs);
} }
+1 -1
View File
@@ -67,7 +67,7 @@ impl_op!(Vec2 Sub sub; x y);
impl_op!(Vec2 Mul mul; x y); impl_op!(Vec2 Mul mul; x y);
impl_op!(Vec2 Div div; x y); impl_op!(Vec2 Div div; x y);
impl const DivOr for Vec2 { const impl DivOr for Vec2 {
fn div_or(self, rhs: Self, other: Self) -> Self { fn div_or(self, rhs: Self, other: Self) -> Self {
Self { Self {
x: self.x.div_or(rhs.x, other.x), x: self.x.div_or(rhs.x, other.x),
+14
View File
@@ -0,0 +1,14 @@
# The compositor `run-headless.sh` starts, because this machine has no
# display. Nothing here is meant to be looked at directly; `grim` is.
#
# No Xwayland: winit talks Wayland natively, and starting an X server is a
# second thing to go wrong for no gain. (`emu`'s config forces it because the
# Android emulator's renderer speaks GLX.)
xwayland disable
# A desktop-shaped output, since this is the desktop half of the port. Larger
# than the window an example opens, so nothing is scaled or clipped.
output HEADLESS-1 mode 1920x1200@60Hz
default_border none
focus_follows_mouse no
+108
View File
@@ -0,0 +1,108 @@
#!/bin/sh
# Run an iris example on this machine, which has no display.
#
# ./run-headless.sh tabs [-- cargo args]
# ./run-headless.sh tabs --shot /tmp/tabs.png --seconds 4
#
# The VM has a virtio-gpu render node (Vulkan 1.4 through Venus, GL 4.6
# through virgl), so wgpu runs on the host's real GPU -- what is missing is
# only a compositor to give winit a surface. So: a headless sway, the same
# trick `emu` uses for the Android emulator, and `grim` to see the result.
#
# It is deliberately *not* `emu`'s compositor. sway tiles, so adding a window
# to the one an emulator is sitting in resizes that emulator's window, and a
# peer session's `emu up` could join at any moment. This one has its own
# socket and its own runtime directory and goes away with the machine.
set -eu
here=$(cd "$(dirname "$0")" && pwd)
run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
seconds=3
shot=""
example=""
while [ $# -gt 0 ]; do
case "$1" in
--shot) shot=$2; shift 2 ;;
--seconds) seconds=$2; shift 2 ;;
--) shift; break ;;
*) example=$1; shift ;;
esac
done
[ -n "$example" ] || { echo "usage: $0 EXAMPLE [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
mkdir -p "$run"
export SWAYSOCK="$run/sway.sock"
# Named rather than left to sway's pid-based default, so a second run reuses
# this compositor instead of starting another beside it.
if ! swaymsg -t get_version >/dev/null 2>&1; then
rm -f "$SWAYSOCK"
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 LIBSEAT_BACKEND=noop \
setsid sway -c "$here/headless.conf" >"$run/sway.log" 2>&1 &
i=0
while [ $i -lt 20 ]; do
swaymsg -t get_version >/dev/null 2>&1 && break
i=$((i + 1)); sleep 0.5
done
swaymsg -t get_version >/dev/null 2>&1 || {
echo "run-headless: compositor did not start; see $run/sway.log" >&2
exit 1
}
fi
# Asked of the compositor rather than guessed: sway takes the first free
# wayland-N, and this machine may already have one.
rm -f "$run/display"
swaymsg exec -- "sh -c 'printf %s \"\$WAYLAND_DISPLAY\" > $run/display'" >/dev/null
i=0
while [ $i -lt 20 ]; do
[ -s "$run/display" ] && break
i=$((i + 1)); sleep 0.5
done
[ -s "$run/display" ] || { echo "run-headless: could not read WAYLAND_DISPLAY" >&2; exit 1; }
WAYLAND_DISPLAY=$(cat "$run/display")
export WAYLAND_DISPLAY
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
cd "$here"
cargo build --example "$example" "$@" >&2
bin="$here/target/debug/examples/$example"
"$bin" >"$run/$example.log" 2>&1 &
pid=$!
trap 'kill "$pid" 2>/dev/null || true' EXIT INT TERM
# Wait for the window to be mapped rather than for a number of seconds. A
# fixed sleep took an all-black screenshot the first time this ran, when sway
# had started in the same invocation and had not composited its output yet --
# which is indistinguishable from an app that draws nothing.
i=0
while [ $i -lt 40 ]; do
kill -0 "$pid" 2>/dev/null || break
swaymsg -t get_tree --raw 2>/dev/null | grep -q "\"pid\":$pid," && break
i=$((i + 1)); sleep 0.25
done
# Then settle, for whatever the example does after its first frame.
i=0
while [ $i -lt "$((seconds * 2))" ]; do
kill -0 "$pid" 2>/dev/null || break
i=$((i + 1)); sleep 0.5
done
if kill -0 "$pid" 2>/dev/null; then
[ -n "$shot" ] && grim "$shot" && echo "run-headless: wrote $shot" >&2
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
status=0
else
wait "$pid" 2>/dev/null || status=$?
echo "run-headless: $example exited early (status ${status:-0})" >&2
status=${status:-1}
fi
echo "--- $example output ---" >&2
cat "$run/$example.log" >&2
exit "$status"
+11
View File
@@ -0,0 +1,11 @@
# iris needs nightly (see the #![feature] list in core/src/lib.rs and src/lib.rs).
# The pin is dated rather than "nightly" because the const-traits feature set
# changes shape between nightlies: on 2026-09-04 the vendored January tree would
# not parse at all, because `impl const Trait for T` had become
# `const impl Trait for T`. A rolling channel turns that into a build that
# breaks unattended on whatever machine Dev Updater happens to build on.
# Advance this deliberately, with the feature list in RUST.md's I0b.
[toolchain]
channel = "nightly-2026-09-03"
components = ["clippy", "rustfmt"]
targets = ["aarch64-linux-android", "x86_64-linux-android"]
+1 -1
View File
@@ -52,7 +52,7 @@ impl UiRenderer {
self.config.width = size.width; self.config.width = size.width;
self.config.height = size.height; self.config.height = size.height;
self.surface.configure(&self.device, &self.config); self.surface.configure(&self.device, &self.config);
self.ui.resize(size, &self.queue); self.ui.resize((size.width, size.height), &self.queue);
} }
fn create_encoder(device: &Device) -> CommandEncoder { fn create_encoder(device: &Device) -> CommandEncoder {