Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0ee7d6e94 | ||
|
|
b6b0928087 | ||
|
|
12221ea025 | ||
|
|
5e23c8b0c0 | ||
|
|
caaa733caa |
No files matched your search
@@ -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
|
||||||
|
|||||||
Generated
-1
@@ -1070,7 +1070,6 @@ dependencies = [
|
|||||||
"fxhash",
|
"fxhash",
|
||||||
"image",
|
"image",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
"winit",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -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,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,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,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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
Executable
+108
@@ -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"
|
||||||
@@ -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"]
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user