Compare commits
5
Commits
88631f5e8b
...
800da46188
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
800da46188 | ||
|
|
00767eed4d | ||
|
|
683db4908a | ||
|
|
8d23a20792 | ||
|
|
d01c105037 |
No files matched your search
@@ -7,6 +7,48 @@ marked **DEFERRED** are ones the agent chose not to decide alone.
|
||||
|
||||
## 2026-09-05
|
||||
|
||||
- **iris no longer asks every device for compute-shader limits it never
|
||||
uses.** `adapter.request_device` (both `iris/src/android/render.rs` and
|
||||
`iris/src/default/render.rs`) used `Limits::default()` plus an override
|
||||
for `max_buffer_size`, and `Limits::default()` unconditionally requests
|
||||
desktop-tier compute limits (`max_compute_workgroups_per_dimension:
|
||||
65535`, per `wgpu_types`) even though nothing in `iris`/`iris-core`
|
||||
creates a `ComputePipeline` or writes a `@compute` shader stage —
|
||||
confirmed by grepping the whole tree, not assumed. That crashed
|
||||
`request_device` outright on the Android emulator's software GL path
|
||||
(`EMU_GPU=software`, `--features force-gles`): SwiftShader's GL reports
|
||||
itself as OpenGL ES 3.0, which has no compute shaders at all, so the
|
||||
adapter's real limit is 0 against the unconditional request for 65535 —
|
||||
`RUST.md`'s "Software mode ... crashes for a third, different reason,"
|
||||
2026-09-05, earlier today. The same would happen on any real
|
||||
GLES-3.0-only Android device, not just the emulator. Fixed by a new
|
||||
`iris_core::device_limits()` (`iris/core/src/render/mod.rs`), shared by
|
||||
both platform backends so the two requests cannot drift, that zeros the
|
||||
six `max_compute_*` fields explicitly rather than switching to a
|
||||
downlevel `Limits` preset — `Limits::downlevel_webgl2_defaults()` was
|
||||
considered and rejected: it also zeros
|
||||
`max_storage_buffers_per_shader_stage`, and `shader.wgsl`'s vertex stage
|
||||
reads four `var<storage>` buffers (rects, glyphs, masks, move_offsets),
|
||||
so that preset would trade the compute crash for a bind-group-layout
|
||||
one on the same downlevel hardware this is meant to support. No
|
||||
capability check or fallback path was needed since nothing is being
|
||||
disabled — the request is simply narrowed to what the pipeline actually
|
||||
uses. `rigs/gpu-probe`'s own mirrored limits (it is deliberately its own
|
||||
crate, not a workspace member, so it cannot call `device_limits()`
|
||||
directly) were updated to match, and confirm `IRIS DEVICE: ok` against
|
||||
this VM's own Vulkan and GL adapters. **Not verified this pass**: the
|
||||
specific SwiftShader-ES-3.0 crash this fixes, on-device — the
|
||||
`EMU_GPU=software` cold boot this needs would have force-restarted this
|
||||
checkout's emulator while another session was actively running its own
|
||||
app on it (`com.example.aiapp` had window focus at the time), so it was
|
||||
left for a pass when the emulator is free rather than disrupting that
|
||||
session. Everything reachable without the emulator is clean: `cargo
|
||||
fmt`/`clippy --workspace --all-targets`/`test --workspace`, `cargo ndk
|
||||
build`/`clippy` for `iris-android-app` with `force-gles`, and
|
||||
`gpu-probe` against this VM's own Vulkan and GL(ES 3.2, which still has
|
||||
compute and so would not have reproduced the crash even before this
|
||||
fix — not a substitute for the real ES-3.0 test).
|
||||
|
||||
- **P0's Compose half is built and smoke-tested on the emulator** — the
|
||||
`bench` build type, the shared `app/bench-fixture/` transcript, and an
|
||||
in-process fake backend (`BenchFixture.kt`/`BenchNetwork.kt`) that
|
||||
@@ -18,6 +60,25 @@ marked **DEFERRED** are ones the agent chose not to decide alone.
|
||||
flagged here because it is the first half of something Iris explicitly
|
||||
asked to see before P1.
|
||||
|
||||
- **P0's iris half is also built and smoke-tested on the emulator,
|
||||
2026-09-05.** A new `bench` Cargo feature on `iris-android-app`, on top
|
||||
of `transcript-screen`: the same checked-in fixture (`include_str!`, no
|
||||
asset pipeline needed), the same 24-swipe scroll loop animated through
|
||||
`List::scroll` and the same 400-event/20s streaming phase through
|
||||
`fold_event`, "Run benchmark"/"Copy report" as named accessible
|
||||
controls, and the same three added report fields (process CPU time,
|
||||
peak RSS, battery current) via direct JNI calls
|
||||
(`bench_jni.rs::PlatformHandle`) since `android_view` has no
|
||||
`BatteryManager`/`ClipboardManager` wrapper of its own. One small public
|
||||
API addition to get there: `AndroidAppState::platform_ready` (`IRIS.md`),
|
||||
a default-no-op lifecycle hook handing an implementor a `JavaVM` +
|
||||
`GlobalRef` it can call Java through from any thread. Packaged with a
|
||||
new `release` build type on `iris-android-app`'s own Gradle project
|
||||
(there was previously only `debug`), signed with the same key
|
||||
`app/build-apk.sh` generates. Smoke run and the full report are in
|
||||
RUST.md's P0 box; not attempted this pass: the real on-phone runs and
|
||||
Iris's pass/fail call, which is the actual gate.
|
||||
|
||||
- **The intermittent touch-scroll dropout is root-caused and fixed: a
|
||||
missed `ACTION_DOWN` hit-test, not the previously-suspected coalesced
|
||||
first `ACTION_MOVE`.** Diagnosed by temporary logcat tracing of every
|
||||
|
||||
@@ -8,6 +8,54 @@ capability that moved. Small and trivial changes do not go here.
|
||||
An entry gives the date, what changed, why, and a short before/after where
|
||||
it helps judge the change without the session that made it. Newest first.
|
||||
|
||||
## 2026-09-05: `AndroidAppState::platform_ready` (RUST.md's P0 box, iris half)
|
||||
|
||||
Added a second, optional lifecycle method to `iris::android::AndroidAppState`
|
||||
(`iris/src/android/view.rs`), called once from `new_peer` right after `new`:
|
||||
|
||||
```rust
|
||||
fn platform_ready(&mut self, rsc: &mut AndroidRsc<Self>, vm: JavaVM, view: GlobalRef) {}
|
||||
```
|
||||
|
||||
Default does nothing, so every existing implementor (`Client`,
|
||||
`TranscriptClient`) is unaffected. It exists for a caller that needs to call
|
||||
into Java itself beyond what a `RequestRedraw` handle already covers --
|
||||
P0's bench build (`iris-android-app`'s new `bench` feature,
|
||||
`bench_client.rs`/`bench_jni.rs`) uses it to hold a `JavaVM` + `GlobalRef`
|
||||
to the view so its "Copy report" control and once-a-second battery sampler
|
||||
can call `BatteryManager`/`ClipboardManager` through the view's own
|
||||
`Context`, from a background tokio task as well as the UI thread. `new`
|
||||
itself was not extended with these two parameters: most implementors need
|
||||
nothing here, and `new`'s job is building the widget tree, not holding a
|
||||
platform handle. `vm`/`view` are independent handles from the ones
|
||||
`new_peer` keeps for its own `RequestRedraw` (a fresh `get_java_vm`/
|
||||
`new_global_ref` each), so storing them has no effect on that mechanism.
|
||||
|
||||
## 2026-09-05 (later still): `iris_core::device_limits()`, and iris no longer requests compute-shader limits
|
||||
|
||||
New public function, `iris_core::device_limits() -> wgpu::Limits`. Why:
|
||||
`adapter.request_device`'s `required_limits` was `Limits::default()` plus
|
||||
a `max_buffer_size` override in both platform backends, and
|
||||
`Limits::default()` requests desktop-tier compute-shader limits
|
||||
unconditionally (`max_compute_workgroups_per_dimension: 65535`) even
|
||||
though nothing in `iris`/`iris-core` uses a `ComputePipeline` — that
|
||||
crashed device creation outright on a downlevel GL adapter reporting
|
||||
OpenGL ES 3.0 (no compute shaders at all: the Android emulator's
|
||||
`EMU_GPU=software` path, and any real GLES-3.0-only Android device).
|
||||
`device_limits()` is what both `android::render::AndroidRenderer::new`
|
||||
and `default::render::UiRenderer::new` now build their `required_limits`
|
||||
from, so the request cannot drift between the two backends.
|
||||
|
||||
Before: `Limits { max_buffer_size: 1 << 30, ..Default::default() }`
|
||||
inlined in each backend. After: `iris_core::device_limits()`, which is
|
||||
the same thing with the six `max_compute_*` fields additionally zeroed.
|
||||
A caller building its own `DeviceDescriptor` outside these two backends
|
||||
(there are none today, but a third platform backend would want this)
|
||||
should call `device_limits()` rather than reaching for
|
||||
`Limits::default()` directly, unless it genuinely adds a compute pass —
|
||||
in which case it wants the specific compute limits that pass needs, not
|
||||
the desktop-tier default for everything.
|
||||
|
||||
## 2026-09-05 (later the same day): `iris_core::FrameReport` (RUST.md's I5 box)
|
||||
|
||||
New public type, `iris_core::FrameReport` (re-exported from `iris_core`'s
|
||||
|
||||
@@ -7,6 +7,29 @@ order and what "done" looks like. Tick and date them in place.
|
||||
|
||||
## Fix
|
||||
|
||||
- [x] **`request_device` asked for compute-shader limits it never uses
|
||||
(2026-09-05).** `Limits::default()` (both `iris/src/android/render.rs`
|
||||
and `iris/src/default/render.rs`) requests desktop-tier compute limits
|
||||
unconditionally, even though nothing in `iris`/`iris-core` creates a
|
||||
`ComputePipeline` or writes a `@compute` shader stage — confirmed by
|
||||
grepping the whole tree, not assumed. That crashed device creation
|
||||
outright on the Android emulator's software GL path (`EMU_GPU=software`,
|
||||
`--features force-gles`): SwiftShader's GL reports itself as OpenGL ES
|
||||
3.0, which has no compute shaders, so the adapter's real limit is 0
|
||||
against the unconditional request for 65535 — the same would happen on
|
||||
any real GLES-3.0-only Android device. Fixed by a new, shared
|
||||
`iris_core::device_limits()` (`iris/core/src/render/mod.rs`) that zeros
|
||||
exactly the six `max_compute_*` fields rather than switching to a
|
||||
downlevel `Limits` preset — `downlevel_webgl2_defaults()` also zeros
|
||||
`max_storage_buffers_per_shader_stage`, which `shader.wgsl`'s vertex
|
||||
stage needs (four `var<storage>` buffers), so that preset would trade
|
||||
this crash for a bind-group-layout one on the same hardware.
|
||||
`rigs/gpu-probe`'s own hand-mirrored `Limits` (it is deliberately its
|
||||
own crate, not able to call `device_limits()` directly) was updated to
|
||||
match. See `DECISIONS.md` and RUST.md's I5 box for the account,
|
||||
including what could not be re-verified on-device this pass (the
|
||||
emulator was in concurrent use by another session).
|
||||
|
||||
- [x] **Input does not fall through by input type (2026-09-04).**
|
||||
`SensorUi::run_sensors` (`src/default/sense.rs`) used to set "consumed,
|
||||
stop checking lower layers" from mere hover — a widget registered for
|
||||
@@ -82,6 +105,18 @@ order and what "done" looks like. Tick and date them in place.
|
||||
were exactly the same root cause measured two different ways. Frame 2
|
||||
now reports 0 (see the numbers above); not a separate fix.
|
||||
|
||||
- [ ] **A read-only text display has no widget of its own — P0's bench
|
||||
report area is a `TextEdit` standing in for one (2026-09-05).** The only
|
||||
way to get selectable text on screen today is `.editable(...)` plus
|
||||
`.attr::<Selectable>(())` (`Selectable` is only implemented for
|
||||
`TextEdit`, `iris/src/attr.rs`), which also makes the field focusable —
|
||||
tapping the bench report opens the soft keyboard over text nothing lets
|
||||
you type into. Harmless for a bench-only debug screen (not fixed this
|
||||
pass), but a real "selectable, not editable" text primitive would
|
||||
remove the keyboard side effect and is worth having before another
|
||||
screen wants the same thing (P1's own transcript rows already read
|
||||
their content from a `TextEdit` for the same reason).
|
||||
|
||||
## Build
|
||||
|
||||
- [x] **Benchmarks**, not unit tests, run on demand (2026-09-05; a
|
||||
|
||||
+228
@@ -59,6 +59,21 @@ session spending an afternoon on them again.
|
||||
end-to-end this pass. The fix itself is verified by direct, targeted
|
||||
logcat traces taken before that interference began, not by the
|
||||
aggregate script.
|
||||
- **iris no longer requests compute-shader limits it never uses,
|
||||
2026-09-05.** `adapter.request_device`'s `Limits::default()` asks for
|
||||
desktop-tier compute limits unconditionally even though nothing in
|
||||
`iris`/`iris-core` uses a `ComputePipeline` -- confirmed by grep, not
|
||||
assumed -- which is what crashed `request_device` outright under
|
||||
`EMU_GPU=software`'s `force-gles` path (SwiftShader's GL reports OpenGL
|
||||
ES 3.0, no compute at all). New shared `iris_core::device_limits()`
|
||||
zeros exactly the six compute fields; `rigs/gpu-probe`'s own mirrored
|
||||
limits were updated and confirm `IRIS DEVICE: ok` on this VM's own
|
||||
Vulkan and GL adapters. **The specific SwiftShader-ES-3.0 crash this
|
||||
fixes was not re-verified on-device this pass** -- the cold boot needed
|
||||
would have force-restarted this checkout's emulator while another
|
||||
session had its own app focused on it, so it was left rather than
|
||||
disrupted. See this box's "Fixed, 2026-09-05, later the same day"
|
||||
subsection (under the software-mode crash it fixes) and `DECISIONS.md`.
|
||||
- **Decided 2026-09-05: iris over Masonry**, by Iris, from the host-GPU
|
||||
numbers in I5's box and E1/E2's findings. See the Recommendation's item
|
||||
3 and `DECISIONS.md`. Next: the remaining screens and the app on iris —
|
||||
@@ -3211,6 +3226,45 @@ silently on real hardware.
|
||||
general) explains the ~80-150ms software-mode numbers, since no GLES
|
||||
number under software mode could be taken at all.
|
||||
|
||||
**Fixed, 2026-09-05, later the same day.** Not "requesting compute
|
||||
limits only when the adapter reports them" (a capability check with
|
||||
a fallback) -- simpler than that, because iris has no code path that
|
||||
needs compute at all: grepped the whole `iris`/`iris-core` tree for
|
||||
`ComputePipeline`/`@compute` and found none, so the right fix is to
|
||||
stop asking for compute limits, full stop, rather than to build a
|
||||
fallback for a capability nothing uses. `iris_core::device_limits()`
|
||||
(`iris/core/src/render/mod.rs`) is the one place both platform
|
||||
backends now build their `required_limits` from: `Limits::default()`
|
||||
with the six `max_compute_*` fields zeroed and `max_buffer_size`
|
||||
still raised, as before. `Limits::downlevel_webgl2_defaults()` was
|
||||
the first thing tried and rejected -- it also zeros
|
||||
`max_storage_buffers_per_shader_stage`, and `shader.wgsl`'s vertex
|
||||
stage reads four `var<storage>` buffers, so it would have traded
|
||||
this crash for a bind-group-layout one on the same hardware.
|
||||
`rigs/gpu-probe`'s own `Limits` (necessarily a hand-mirrored copy --
|
||||
that rig is deliberately its own crate, not a workspace member) was
|
||||
updated to match and re-run: `IRIS DEVICE: ok` against this VM's own
|
||||
Vulkan (Venus) and GL (virgl, reports OpenGL ES 3.2) adapters.
|
||||
**Not verified against the actual SwiftShader-ES-3.0 failure this
|
||||
pass**: the `EMU_GPU=software` cold boot needed to reproduce it would
|
||||
have force-restarted this checkout's shared emulator while another
|
||||
session had `com.example.aiapp` focused and running on it (`adb
|
||||
shell dumpsys window`), so this pass left that measurement rather
|
||||
than disrupting concurrent work -- matching AGENTS.md's "coordinate
|
||||
with peer agents" guidance rather than contending for the emulator.
|
||||
Everything else: `cargo fmt --all`/`clippy --workspace --all-targets`/
|
||||
`test --workspace` clean, `cargo ndk build`/`clippy` for
|
||||
`iris-android-app --features transcript-screen,force-gles` clean
|
||||
(only the pre-existing unused-`tabs-ui`-dependency warning, unrelated
|
||||
to this change). This also means the software-mode question two
|
||||
boxes up is still open, for the same original reason plus this new
|
||||
one: a GLES number under `EMU_GPU=software` still has not been
|
||||
taken, now blocked on emulator availability rather than on the
|
||||
crash. A future pass should cold-boot `EMU_GPU=software` once the
|
||||
emulator is free, confirm `dev.iris.android.demo` no longer aborts
|
||||
on `request_device`, and take the `iris-scroll.sh` FrameReport row
|
||||
that pairs with this box's host-GPU one.
|
||||
|
||||
**Verification, this update.** `cargo fmt --all` (no diff),
|
||||
`cargo clippy --workspace --all-targets` (no warnings from the new
|
||||
code; pre-existing `wgpu`/`winit`/`naga` future-incompat notices
|
||||
@@ -3487,6 +3541,180 @@ device.
|
||||
this session was told not to touch `iris/`), and anything past the
|
||||
emulator — the actual on-phone runs and Iris's pass/fail call.
|
||||
|
||||
**iris half: done, 2026-09-05.** A `bench` Cargo feature on
|
||||
`iris-android-app`, built on top of `transcript-screen`
|
||||
(`bench = ["transcript-screen", "dep:libc", "dep:tokio"]`,
|
||||
`iris/android-app/Cargo.toml`), gives `lib.rs`'s `ActiveClient`
|
||||
priority a third `AndroidAppState` (`bench_client::BenchClient`)
|
||||
over `TranscriptClient` when both features are listed together --
|
||||
matching the exact build command below, which lists both.
|
||||
|
||||
**Fixture.** `include_str!("../../../app/bench-fixture/assets/
|
||||
transcript.jsonl")` (1,915,760 bytes) at compile time -- no asset
|
||||
pipeline needed the way the Compose half's Gradle source set does.
|
||||
`bench_client::parse_fixture` splits the same way `BenchFixture.kt`
|
||||
does: the first 3,200 non-blank lines parsed as `serde_json::Value`s
|
||||
and folded once through `client_core::transcript_fold::fold_page`
|
||||
(the real fold a `/transcript` page goes through), the rest parsed
|
||||
as `event_model::SeqEvent`s and held back as the streaming tail.
|
||||
`build.rs` (transcript-screen's own) now exits early under `bench`
|
||||
before requiring a live server's host/port/token/CA -- `BenchClient`
|
||||
never calls `build_transport()`, so that requirement made no sense
|
||||
for a build that talks to nothing.
|
||||
|
||||
**"Run benchmark" (`.label("Run benchmark")`) and "Copy report"
|
||||
(`.label("Copy report")`)** sit in a fixed bar above the transcript;
|
||||
a selectable `TextEdit` (`.attr::<Selectable>(())`, the same
|
||||
attribute the composer field uses) below it shows the report text.
|
||||
Pressing "Run benchmark" resets `FrameReport`, then drives
|
||||
`List::scroll` in ~60Hz steps (`ANIM_STEP_MS = 16`) to animate each
|
||||
900px/200ms swipe rather than jumping it -- iris's `List` has no
|
||||
built-in tween the way `animateScrollBy(tween(...))` gives Compose,
|
||||
so this is the one place the two backends' bench code has to differ
|
||||
in shape rather than only in numbers -- through the same
|
||||
`rsc.tasks.redraw_handle()` + manual `request_redraw()` per step
|
||||
`transcript_client.rs` already established (a `Tasks::spawn`d
|
||||
future's *automatic* redraw fires once, after the whole future
|
||||
completes, which would show nothing moving until the run ends).
|
||||
After the scroll loop, `List::jump_to_end()` pins to the newest
|
||||
content (matching `stream-bench.sh`'s "Jump to latest" tap), then
|
||||
400 fixture events replay at 20/s through `fold_event` -- the same
|
||||
fold path a live SSE frame takes in `transcript_client.rs`'s own
|
||||
`apply_event` -- each one triggering `rebuild_transcript`'s full
|
||||
`transcript_ui::build_tree` rebuild, same tradeoff as
|
||||
`TranscriptClient`/`desktop-app`. A battery sampler runs
|
||||
concurrently on its own `tokio::spawn`d task (not through
|
||||
`ctx.update`, since a JNI battery read needs no widget-tree access),
|
||||
attaching whichever thread it runs on via a stored `JavaVM` --
|
||||
`AndroidAppState::platform_ready` (new, `IRIS.md`) is what hands
|
||||
`bench_client.rs` that `JavaVM` + a `GlobalRef` to the view, since
|
||||
neither was reachable from `AndroidAppState::new` before this box.
|
||||
|
||||
**Report fields.** `FrameStats`'s existing `Display` (frames, janky
|
||||
%, p50/p90/p99, worst, and I5's own `cpu_p50`/`gpu_wait_p50` CPU/GPU
|
||||
split) plus a `bench:`-shaped tail this box added: process CPU time
|
||||
via `libc::getrusage(RUSAGE_SELF)` (user+system time; chosen over
|
||||
parsing `/proc/self/stat` by hand to avoid assuming `USER_HZ`), peak
|
||||
RSS from `/proc/self/status`'s `VmHWM` (same source `BenchRun.kt`
|
||||
reads), and battery current sampled once a second via
|
||||
`BatteryManager.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW)`
|
||||
through direct JNI calls (`bench_jni.rs`'s `PlatformHandle` --
|
||||
`android_view::context`'s own `Context`/`Resources` wrappers have no
|
||||
`getSystemService`, so this calls it directly rather than growing
|
||||
that crate's wrapper for two one-off calls). `0`/`Integer.MIN_VALUE`
|
||||
read as "unavailable" rather than folded into the average, matching
|
||||
`BatterySampler`'s own rule and UI_RULES.md's "never present an
|
||||
inferred value as a measured one." The report is logged under the
|
||||
existing `iris-android-app` logcat tag on a line starting `iris
|
||||
bench report:` (grep-able the same way `transcript_client.rs`'s
|
||||
"Frame report" control already is), shown in the on-screen
|
||||
`TextEdit`, and copied to the system clipboard by "Copy report"
|
||||
through `ClipboardManager.setPrimaryClip` (`bench_jni.rs`, same
|
||||
`PlatformHandle`).
|
||||
|
||||
**Build commands, all clean this pass:**
|
||||
- `cargo fmt --all -- --check` (iris workspace) and
|
||||
`cd iris/android-app && cargo fmt --all -- --check`: clean.
|
||||
- `cargo clippy --workspace --all-targets` (iris workspace): clean
|
||||
(only the pre-existing `wgpu`/`winit`/`naga` future-incompat
|
||||
notice).
|
||||
- `cargo test --workspace` (iris workspace): 39 + 8 + 10 = the same
|
||||
pre-existing counts, all passing, unaffected by this box (it
|
||||
touched no logic under test there beyond `AndroidAppState`'s new
|
||||
default no-op method).
|
||||
- `cargo ndk -t x86_64 -P 26 clippy --features "transcript-screen
|
||||
force-gles bench" --lib -- -D warnings` (`iris/android-app`):
|
||||
clean.
|
||||
- `cargo ndk -t arm64-v8a -P 26 -o app/src/main/jniLibs/ build
|
||||
--release --features "transcript-screen force-gles bench"`:
|
||||
clean, `arm64-v8a/libmain.so` produced. The pre-existing "unused
|
||||
dependency `tabs-ui`" Cargo advisory also appears on a plain
|
||||
`--features transcript-screen` build with no `bench` (confirmed
|
||||
by building that combination alone with fake env vars) -- not
|
||||
something this box introduced, and not a clippy/rustc warning
|
||||
(AGENTS.md's "keep the build clean" gate is `cargo clippy`, which
|
||||
stays silent on it).
|
||||
|
||||
**Packaging.** No `cargo xtask apk` exists for `iris/android-app`
|
||||
yet (I2's own Gradle project is the only pipeline), so this reused
|
||||
that split rather than inventing one: `cargo ndk --release` above
|
||||
builds the cdylib straight into `app/src/main/jniLibs/`, then a new
|
||||
`release` build type in `app/build.gradle` (there was previously
|
||||
only `debug`) packages and signs it --
|
||||
`AI_APP_KEYSTORE=~/.config/ai-app/release.jks` +
|
||||
`AI_APP_KEYSTORE_PASSWORD` (the same key `app/build-apk.sh`
|
||||
generates for the Compose app) via `gradle :app:assembleRelease`,
|
||||
with `applicationIdSuffix ".bench"` so it installs beside the plain
|
||||
tabs demo rather than replacing it. `aapt2 dump badging` on the
|
||||
result: `package: name='dev.iris.android.demo.bench'`, one native
|
||||
library, `lib/arm64-v8a/libmain.so`. `apksigner verify
|
||||
--print-certs` shows the same `CN=ai-app` certificate
|
||||
`compose-bench-arm64.apk` is signed with.
|
||||
|
||||
**Emulator smoke run, 2026-09-05.** This checkout's own AVD
|
||||
(`ai-app-2`) was in use by the session recording I5's clean-scroll
|
||||
comparison in this same file (its Compose app was in the
|
||||
foreground, confirmed via `dumpsys window`/`dumpsys activity
|
||||
processes` before touching anything) -- rather than contend for it
|
||||
(AGENTS.md's "coordinate with peer agents"), a second,
|
||||
differently-named AVD was created (`AVD_NAME=ai-app-2-bench emu
|
||||
up`, `pixel_10`/`android-36`/`google_apis`/`x86_64`, cold boot, host
|
||||
GPU, no `EMU_GPU=software`), with 12GB of the VM's memory still
|
||||
available after both were up (this-machine-android's "two are
|
||||
comfortable" guidance). Installed via `adb -s emulator-5556 install
|
||||
-r`, launched, driven by `ui-trace record -s emulator-5556 --do
|
||||
"tap 'Run benchmark'"` (the control resolved by its accessibility
|
||||
label, per AGENTS.md's "no coordinate" rule), then read back over
|
||||
`adb logcat`:
|
||||
|
||||
iris bench report
|
||||
frames=372 janky%=56.99 p50=19.5ms p90=219.5ms p99=284.5ms worst=369.3ms (measures redraw-start to after present() is called, not GPU/compositor completion) cpu_p50=0.4ms gpu_wait_p50=13.9ms (redraw-start-to-submit vs. submit-to-after-present)
|
||||
scroll: 6 cycles (24 swipes), streamed 400/400 fixture events
|
||||
process CPU time over this run: 24665ms
|
||||
peak RSS: 224600kB
|
||||
battery current: mean 900000µA over 21 samples (min 900000, max 900000)
|
||||
|
||||
"Copy report" was pressed immediately after and logged `iris bench
|
||||
report: copied to clipboard` (`ClipboardManager.setPrimaryClip`
|
||||
succeeded). No crash (`adb logcat`'s `FATAL`/`AndroidRuntime` lines
|
||||
checked -- only `ui-trace`'s own runtime, unrelated), process alive
|
||||
throughout (`dumpsys activity processes`), 400/400 stream events
|
||||
confirmed sent.
|
||||
|
||||
Read this the same way the Compose half's own box already asks to
|
||||
read its number: this is software-rasterised (well, GLES-over-virgl
|
||||
under `force-gles`, per I5's "Where iris's frame time goes")
|
||||
emulator output, "the harness runs end to end and produces every
|
||||
field P0 asked for," not a phone number -- and the battery current
|
||||
is again the emulator's fixed 900000µA mocked charger reporting a
|
||||
constant, exactly what the Compose box's own run found, not a real
|
||||
battery answering. `cpu_p50=0.4ms` (iris's own per-frame CPU work)
|
||||
against a much larger `gpu_wait_p50`/`p50` again matches I5's "Where
|
||||
iris's frame time goes" finding under real GPU rendering (`-gpu
|
||||
host`, `force-gles`) -- the frame-time budget here is dominated by
|
||||
the driver/compositor wait, not by iris's layout or primitive
|
||||
building, though this run's `janky%`/`p90`/`p99` are considerably
|
||||
worse than that earlier isolated pass, most likely the cost of this
|
||||
AVD's very first cold boot plus running two emulators on this VM at
|
||||
once (a fair comparison against Compose would need both apps run
|
||||
back-to-back on the same freshly-booted device, not attempted this
|
||||
pass since the second AVD was torn down immediately after per
|
||||
AGENTS.md's "stop yours when you are done with it").
|
||||
|
||||
Copied to `~/host/bench/iris-bench-arm64.apk` (15,445,468 bytes) and
|
||||
`~/host/bench/README.md`'s "iris" section filled in (install, open,
|
||||
tap "Run benchmark", read the report from the on-screen text or
|
||||
logcat, tap "Copy report", paste back).
|
||||
|
||||
**Not done this pass**: the actual on-phone runs and Iris's
|
||||
pass/fail call between the two reports (P0's own pass condition) --
|
||||
that needs Iris's phone, which this session has no access to.
|
||||
`iris/src/android/view.rs` was touched (`AndroidAppState::
|
||||
platform_ready`, `new_peer`'s wiring) -- confirmed to not be one of
|
||||
the three files the concurrent `device_limits()` work on this
|
||||
branch was using (`iris/core/src/render/mod.rs`,
|
||||
`iris/src/android/render.rs`, `iris/src/default/render.rs`).
|
||||
|
||||
- [ ] **P1 — session screen parity.** History paging backward (with the
|
||||
page-boundary healing `client-core` does not have yet, below),
|
||||
`TranscriptSource`-backed cache/server stitching, jump-to-latest,
|
||||
|
||||
Generated
+2
@@ -1768,9 +1768,11 @@ dependencies = [
|
||||
"client-core",
|
||||
"event-model",
|
||||
"iris",
|
||||
"libc",
|
||||
"log",
|
||||
"serde_json",
|
||||
"tabs-ui",
|
||||
"tokio",
|
||||
"transcript-ui",
|
||||
]
|
||||
|
||||
|
||||
@@ -32,6 +32,23 @@ transcript-ui = { path = "../transcript-ui", optional = true }
|
||||
client-core = { path = "../../client-core", optional = true }
|
||||
event-model = { path = "../../event-model", optional = true }
|
||||
serde_json = { version = "1", features = ["float_roundtrip"], optional = true }
|
||||
# P0's bench build only (docs/RUST.md): `getrusage(RUSAGE_SELF)` for
|
||||
# process CPU time, matching `libc::getrusage`'s mention in that box over
|
||||
# parsing `/proc/self/stat` by hand and assuming `USER_HZ`. Already in the
|
||||
# workspace's own dependency tree transitively (`iris/Cargo.lock`, pinned
|
||||
# at 0.2.179) -- this makes it a direct dependency at the same version
|
||||
# rather than a second, possibly-drifting resolution.
|
||||
libc = { version = "0.2.179", optional = true }
|
||||
# P0's bench build only: the scroll animation and the streaming phase are
|
||||
# both a sequence of `sleep`s inside the async task `rsc.spawn_task` already
|
||||
# runs on iris's own tokio runtime (`iris/src/task.rs`'s `Tasks::init`), and
|
||||
# the battery sampler is a second, concurrent task on that same runtime
|
||||
# (`tokio::spawn`) -- so this crate needs `tokio` directly rather than only
|
||||
# through `iris`. `rt`+`time` only: no I/O, no macros, nothing this crate
|
||||
# doesn't call. Version matches the one `iris`'s own dependency tree already
|
||||
# resolves to (`iris/Cargo.lock`), so there is one copy of the runtime, not
|
||||
# two.
|
||||
tokio = { version = "1.53.1", features = ["rt", "time"], optional = true }
|
||||
|
||||
[features]
|
||||
default = ["tabs-screen"]
|
||||
@@ -41,6 +58,14 @@ transcript-screen = ["dep:transcript-ui", "dep:client-core", "dep:event-model",
|
||||
# instead of SwiftShader's software Vulkan. See `iris/Cargo.toml`'s own doc
|
||||
# on the feature this forwards to.
|
||||
force-gles = ["iris/force-gles"]
|
||||
# P0's iris half (docs/RUST.md, docs/AGENTS.md's "The rigs"): the same
|
||||
# checked-in fixture, scroll loop and streaming phase the Compose `bench`
|
||||
# build type drives, run here against `transcript-ui`'s real screen with no
|
||||
# server. Depends on `transcript-screen` for `transcript-ui`/`client-core`/
|
||||
# `event-model` -- `lib.rs`'s `ActiveClient` selection gives this feature
|
||||
# priority over `transcript-screen`'s own `TranscriptClient` when both are
|
||||
# listed, which is how this crate's build command names both explicitly.
|
||||
bench = ["transcript-screen", "dep:libc", "dep:tokio"]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
@@ -19,9 +19,39 @@ android {
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
// A release build must be signed, and the key is per machine rather than per repo -- same
|
||||
// reasoning and the same key as `app/build-apk.sh` (the Compose app): it is what a phone
|
||||
// recognises the app by, and a secret never lives in a checkout (the mount is shared with an
|
||||
// untrusted VM). `build-apk.sh` generates this key once and points at it through the
|
||||
// environment; without it a release build here is unsigned, which is fine for everything
|
||||
// except installing.
|
||||
def keystore = System.getenv("AI_APP_KEYSTORE")
|
||||
signingConfigs {
|
||||
if (keystore != null) {
|
||||
release {
|
||||
storeFile = file(keystore)
|
||||
storePassword = System.getenv("AI_APP_KEYSTORE_PASSWORD")
|
||||
keyAlias = "ai-app"
|
||||
keyPassword = storePassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
}
|
||||
// P0's iris half (docs/RUST.md's P0 box): the build a phone actually runs. The `.so`
|
||||
// itself is built separately with `cargo ndk --release --features "transcript-screen
|
||||
// force-gles bench"` straight into src/main/jniLibs/ (this crate's own Cargo.toml) --
|
||||
// Gradle here only packages and signs whatever is already there, the same division as the
|
||||
// debug/tabs-screen build this project started with. `applicationIdSuffix` keeps it
|
||||
// installable beside a debug build of the tabs demo rather than replacing it.
|
||||
release {
|
||||
applicationIdSuffix ".bench"
|
||||
if (keystore != null) {
|
||||
signingConfig = signingConfigs.release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
|
||||
@@ -22,6 +22,14 @@ fn main() {
|
||||
if std::env::var_os("CARGO_FEATURE_TRANSCRIPT_SCREEN").is_none() {
|
||||
return;
|
||||
}
|
||||
// P0's bench build (docs/RUST.md) opens the checked-in fixture with no
|
||||
// server at all -- `bench_client.rs` never references the `pinned`
|
||||
// module this generates, so requiring a live server's host/port/token/
|
||||
// CA to build it (as plain `transcript-screen` does, below) would be a
|
||||
// pointless requirement for a build that talks to nothing.
|
||||
if std::env::var_os("CARGO_FEATURE_BENCH").is_some() {
|
||||
return;
|
||||
}
|
||||
println!("cargo:rerun-if-env-changed=AI_APP_TRANSCRIPT_HOST");
|
||||
println!("cargo:rerun-if-env-changed=AI_APP_TRANSCRIPT_PORT");
|
||||
println!("cargo:rerun-if-env-changed=AI_APP_TRANSCRIPT_TOKEN");
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
//! P0's iris half (docs/RUST.md's P0 box, docs/AGENTS.md's "The rigs"):
|
||||
//! the same fixture, scroll loop and streaming phase the Compose `bench`
|
||||
//! build type's `BenchRun.kt`/`BenchFixture.kt` drive, run here against
|
||||
//! `transcript-ui`'s real screen with no server -- a frame-time comparison
|
||||
//! that measures the renderer rather than the data or the network.
|
||||
//!
|
||||
//! **Reuses `transcript_client.rs`'s shape** (folded items, a full
|
||||
//! `transcript_ui::build_tree` rebuild per event) with the network half
|
||||
//! replaced by the checked-in fixture, embedded with `include_str!` --
|
||||
//! `app/bench-fixture/assets/transcript.jsonl`, 1,915,760 bytes, generated
|
||||
//! by `app/bench-fixture/generate.py` and never a real transcript (that
|
||||
//! file's own README). The first 3,200 lines are the opening backlog,
|
||||
//! folded once through `client_core::transcript_fold::fold_page` exactly
|
||||
//! as a real `/transcript` page would be; the remaining ~400 are the
|
||||
//! streaming tail, replayed one at a time through `fold_event` -- the same
|
||||
//! fold path a live SSE reply arrives on -- by the "Run benchmark"
|
||||
//! control below.
|
||||
|
||||
use crate::bench_jni::PlatformHandle;
|
||||
use android_view::jni::{JavaVM, objects::GlobalRef};
|
||||
use client_core::transcript_fold::{TranscriptItem, fold_event, fold_page, group_tool_runs};
|
||||
use event_model::SeqEvent;
|
||||
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
|
||||
use iris::prelude::*;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
/// bench-fixture/README.md: the first `BACKLOG_COUNT` non-blank lines are
|
||||
/// the opening window; the rest are the streaming tail. Kept in sync with
|
||||
/// `BenchFixture.kt`'s identical constant by hand -- both read the same
|
||||
/// checked-in file, so a mismatch would only mean the two apps' bench
|
||||
/// builds open a different split of it, not a wrong-vs-right answer.
|
||||
const BACKLOG_COUNT: usize = 3200;
|
||||
|
||||
/// `BenchRun.kt`'s own constants -- kept identical so the two apps' bench
|
||||
/// runs are the same gesture and the same load, which is the entire point
|
||||
/// of a shared fixture and a shared scripted loop (P0's pass condition).
|
||||
const CYCLES: usize = 6;
|
||||
const SWIPE_PX: f32 = 900.0;
|
||||
const SWIPE_MS: u64 = 200;
|
||||
const SWIPE_PAUSE_MS: u64 = 500;
|
||||
const STREAM_EVENTS_PER_SEC: u64 = 20;
|
||||
const STREAM_SECONDS: u64 = 20;
|
||||
/// One animation step's target cadence -- close enough to 60Hz that a
|
||||
/// `List::scroll` swipe is many small moves rather than one jump, so
|
||||
/// frames are actually rendered along the way (the point of animating it
|
||||
/// at all rather than calling `scroll` once per swipe).
|
||||
const ANIM_STEP_MS: u64 = 16;
|
||||
|
||||
const FIXTURE_JSONL: &str = include_str!("../../../app/bench-fixture/assets/transcript.jsonl");
|
||||
|
||||
pub struct BenchClient {
|
||||
ui_state: AndroidUiState,
|
||||
content: WeakWidget<WidgetPtr>,
|
||||
report_display: WeakWidget<TextEdit>,
|
||||
screen: Option<transcript_ui::TranscriptScreen>,
|
||||
items: Vec<TranscriptItem>,
|
||||
/// The events not yet streamed -- consumed by `start_benchmark`'s own
|
||||
/// clone, kept here only as the source a second run would need (the
|
||||
/// button can be pressed more than once; `running` just stops overlap,
|
||||
/// not repeat).
|
||||
stream_tail: Vec<SeqEvent>,
|
||||
platform: Option<Arc<PlatformHandle>>,
|
||||
last_report: Option<String>,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
impl HasAndroidUiState for BenchClient {
|
||||
fn android_state(&self) -> &AndroidUiState {
|
||||
&self.ui_state
|
||||
}
|
||||
fn android_state_mut(&mut self) -> &mut AndroidUiState {
|
||||
&mut self.ui_state
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the fixture once: `serde_json::Value`s for the backlog
|
||||
/// (`fold_page` takes a page of raw wire JSON, same as a real
|
||||
/// `/transcript` response) and folded `SeqEvent`s for the tail (`fold_event`
|
||||
/// takes one live wire event at a time, same as a real SSE frame).
|
||||
fn parse_fixture() -> (Vec<serde_json::Value>, Vec<SeqEvent>) {
|
||||
let lines: Vec<&str> = FIXTURE_JSONL
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.collect();
|
||||
let mut backlog = Vec::with_capacity(BACKLOG_COUNT.min(lines.len()));
|
||||
let mut stream_tail = Vec::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(line).expect("bench fixture is generated JSON, always valid");
|
||||
if i < BACKLOG_COUNT {
|
||||
backlog.push(value);
|
||||
} else {
|
||||
let event: SeqEvent = serde_json::from_value(value)
|
||||
.expect("bench fixture event matches event-model's SeqEvent");
|
||||
stream_tail.push(event);
|
||||
}
|
||||
}
|
||||
(backlog, stream_tail)
|
||||
}
|
||||
|
||||
fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
|
||||
wtext(message.to_string())
|
||||
.color(Color::WHITE)
|
||||
.wrap(true)
|
||||
.pad(16)
|
||||
.add_strong(rsc)
|
||||
.any()
|
||||
}
|
||||
|
||||
/// `getrusage(RUSAGE_SELF)`'s user+system time, in ms -- `None` only if
|
||||
/// the syscall itself fails, which UI_RULES.md's "never present an
|
||||
/// inferred value as a measured one" says to keep apart from a real (and
|
||||
/// here, impossible) zero.
|
||||
fn process_cpu_ms() -> Option<u64> {
|
||||
// SAFETY: `rusage` is a plain-old-data struct `getrusage` fully
|
||||
// initialises on success; on failure it is never read.
|
||||
unsafe {
|
||||
let mut usage: libc::rusage = std::mem::zeroed();
|
||||
if libc::getrusage(libc::RUSAGE_SELF, &mut usage) != 0 {
|
||||
return None;
|
||||
}
|
||||
let user_ms = usage.ru_utime.tv_sec as u64 * 1000 + usage.ru_utime.tv_usec as u64 / 1000;
|
||||
let sys_ms = usage.ru_stime.tv_sec as u64 * 1000 + usage.ru_stime.tv_usec as u64 / 1000;
|
||||
Some(user_ms + sys_ms)
|
||||
}
|
||||
}
|
||||
|
||||
/// `VmHWM` from `/proc/self/status` -- the process's peak RSS since it
|
||||
/// started, in kB. Same source `BenchRun.kt`'s `peakRssLine` reads, so the
|
||||
/// two reports' numbers mean the same thing.
|
||||
fn peak_rss_kb() -> Option<u64> {
|
||||
std::fs::read_to_string("/proc/self/status")
|
||||
.ok()?
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("VmHWM:"))
|
||||
.and_then(|rest| rest.trim().strip_suffix("kB"))
|
||||
.and_then(|n| n.trim().parse().ok())
|
||||
}
|
||||
|
||||
fn battery_line(samples: &[i32]) -> String {
|
||||
if samples.is_empty() {
|
||||
return " battery current: unavailable on this device".to_string();
|
||||
}
|
||||
let mean = samples.iter().map(|&v| v as i64).sum::<i64>() / samples.len() as i64;
|
||||
let min = samples.iter().min().unwrap();
|
||||
let max = samples.iter().max().unwrap();
|
||||
format!(
|
||||
" battery current: mean {mean}\u{b5}A over {} samples (min {min}, max {max})",
|
||||
samples.len()
|
||||
)
|
||||
}
|
||||
|
||||
impl AndroidAppState for BenchClient {
|
||||
fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self {
|
||||
let content = WidgetPtr::new().add(rsc);
|
||||
let loading = placeholder(rsc, "Loading fixture...");
|
||||
content(rsc).set(loading);
|
||||
|
||||
let report_display = wtext("")
|
||||
.editable(EditMode::MultiLine)
|
||||
.text_align(Align::LEFT)
|
||||
.wrap(true)
|
||||
.size(14)
|
||||
.color(Color::WHITE)
|
||||
.attr::<Selectable>(())
|
||||
.label("Benchmark report")
|
||||
.add(rsc);
|
||||
|
||||
let controls = bench_controls(rsc);
|
||||
let tree = (
|
||||
controls,
|
||||
content.height(rest(2)),
|
||||
report_display.height(rest(1)).pad(8),
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.add_strong(rsc)
|
||||
.any();
|
||||
ui_state.set_root(tree);
|
||||
|
||||
let mut client = Self {
|
||||
ui_state,
|
||||
content,
|
||||
report_display,
|
||||
screen: None,
|
||||
items: Vec::new(),
|
||||
stream_tail: Vec::new(),
|
||||
platform: None,
|
||||
last_report: None,
|
||||
running: false,
|
||||
};
|
||||
|
||||
let (backlog, stream_tail) = parse_fixture();
|
||||
client.stream_tail = stream_tail;
|
||||
match fold_page(&backlog) {
|
||||
Ok(items) => {
|
||||
client.items = items;
|
||||
client.rebuild_transcript(rsc);
|
||||
}
|
||||
Err(message) => {
|
||||
client.show_message(rsc, &format!("Couldn't fold the bench fixture: {message}"))
|
||||
}
|
||||
}
|
||||
client
|
||||
}
|
||||
|
||||
fn platform_ready(&mut self, _rsc: &mut AndroidRsc<Self>, vm: JavaVM, view: GlobalRef) {
|
||||
self.platform = Some(Arc::new(PlatformHandle::new(vm, view)));
|
||||
}
|
||||
|
||||
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>, _render: &mut UiRenderState) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
type Rsc = AndroidRsc<BenchClient>;
|
||||
|
||||
fn bench_controls(rsc: &mut Rsc) -> WeakWidget {
|
||||
let run_rect = rect(Color::rgb(40, 70, 40))
|
||||
.on(
|
||||
CursorSense::click(),
|
||||
|ctx: EventIdCtx<'_, Rsc, _, _>, rsc: &mut Rsc| {
|
||||
ctx.state.start_benchmark(rsc);
|
||||
},
|
||||
)
|
||||
.label("Run benchmark");
|
||||
let run = (
|
||||
run_rect,
|
||||
wtext("Run benchmark").size(18).text_align(Align::CENTER),
|
||||
)
|
||||
.stack()
|
||||
.pad(8)
|
||||
.add(rsc);
|
||||
|
||||
let copy_rect = rect(Color::rgb(50, 50, 60))
|
||||
.on(
|
||||
CursorSense::click(),
|
||||
|ctx: EventIdCtx<'_, Rsc, _, _>, _rsc: &mut Rsc| {
|
||||
ctx.state.copy_report();
|
||||
},
|
||||
)
|
||||
.label("Copy report");
|
||||
let copy = (
|
||||
copy_rect,
|
||||
wtext("Copy report").size(18).text_align(Align::CENTER),
|
||||
)
|
||||
.stack()
|
||||
.pad(8)
|
||||
.add(rsc);
|
||||
|
||||
(run, copy).span(Dir::RIGHT).height(56).add(rsc)
|
||||
}
|
||||
|
||||
impl BenchClient {
|
||||
fn show_message(&mut self, rsc: &mut Rsc, message: &str) {
|
||||
let widget = placeholder(rsc, message);
|
||||
(self.content)(rsc).set(widget);
|
||||
self.screen = None;
|
||||
}
|
||||
|
||||
fn rebuild_transcript(&mut self, rsc: &mut Rsc) {
|
||||
let rows = group_tool_runs(&self.items);
|
||||
let (screen, tree) = transcript_ui::build_tree(rsc, rows);
|
||||
(self.content)(rsc).set(tree);
|
||||
self.screen = Some(screen);
|
||||
}
|
||||
|
||||
fn copy_report(&mut self) {
|
||||
let Some(report) = &self.last_report else {
|
||||
log::info!("iris bench report: nothing to copy -- run the benchmark first");
|
||||
return;
|
||||
};
|
||||
let Some(platform) = &self.platform else {
|
||||
log::info!("iris bench report: no platform handle, can't reach the clipboard");
|
||||
return;
|
||||
};
|
||||
if platform.copy_to_clipboard("iris bench report", report) {
|
||||
log::info!("iris bench report: copied to clipboard");
|
||||
} else {
|
||||
log::info!("iris bench report: clipboard copy failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// P0's scripted run: `BenchRun.kt`'s scroll loop, then its streaming
|
||||
/// phase, then the report -- run in-process for the same reason that
|
||||
/// file's own doc gives (no usable system tracing on a real phone, no
|
||||
/// agent that can drive one).
|
||||
fn start_benchmark(&mut self, rsc: &mut Rsc) {
|
||||
if self.running {
|
||||
log::info!("iris bench report: already running");
|
||||
return;
|
||||
}
|
||||
self.running = true;
|
||||
self.android_state_mut().frame_report.reset();
|
||||
self.report_display.edit(rsc).set("Running benchmark...");
|
||||
|
||||
let redraw = rsc.tasks.redraw_handle();
|
||||
let platform = self.platform.clone();
|
||||
let stream_tail = self.stream_tail.clone();
|
||||
let cpu_start = process_cpu_ms();
|
||||
|
||||
rsc.spawn_task(async move |mut ctx| {
|
||||
// The swipe loop: two drags toward newer content, two back --
|
||||
// a cycle returns to where it started, so the whole loop
|
||||
// measures steady-state scrolling. `BenchRun.kt`'s own
|
||||
// comment on this shape.
|
||||
for _ in 0..CYCLES {
|
||||
for delta in [SWIPE_PX, SWIPE_PX, -SWIPE_PX, -SWIPE_PX] {
|
||||
animate_scroll(&mut ctx, &redraw, delta, SWIPE_MS).await;
|
||||
tokio::time::sleep(Duration::from_millis(SWIPE_PAUSE_MS)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Pinned to the newest end before streaming starts, matching
|
||||
// `stream-bench.sh`'s "Jump to latest" tap.
|
||||
ctx.update(|state: &mut BenchClient, rsc| {
|
||||
if let Some(screen) = &state.screen {
|
||||
(screen.list)(rsc).jump_to_end();
|
||||
}
|
||||
});
|
||||
redraw.request_redraw();
|
||||
|
||||
// The battery sampler runs concurrently with the streaming
|
||||
// phase, once a second, the same cadence `BatterySampler` uses
|
||||
// on the Compose side -- via its own JNI-attached thread, not
|
||||
// `ctx.update`, since a sample needs no widget-tree access.
|
||||
let sampler_done = Arc::new(AtomicBool::new(false));
|
||||
let samples = Arc::new(std::sync::Mutex::new(Vec::<i32>::new()));
|
||||
let sampler = platform.clone().map(|platform| {
|
||||
let done = sampler_done.clone();
|
||||
let samples = samples.clone();
|
||||
tokio::spawn(async move {
|
||||
while !done.load(Ordering::Relaxed) {
|
||||
if let Some(value) = platform.battery_current_ua() {
|
||||
samples.lock().unwrap().push(value);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let total = (STREAM_EVENTS_PER_SEC * STREAM_SECONDS) as usize;
|
||||
let mut sent = 0usize;
|
||||
for event in stream_tail.into_iter().take(total) {
|
||||
ctx.update(move |state: &mut BenchClient, rsc| {
|
||||
state.items = fold_event(&state.items, &event);
|
||||
state.rebuild_transcript(rsc);
|
||||
});
|
||||
redraw.request_redraw();
|
||||
sent += 1;
|
||||
tokio::time::sleep(Duration::from_millis(1000 / STREAM_EVENTS_PER_SEC)).await;
|
||||
}
|
||||
// Lets the last few deltas land and draw before the report is
|
||||
// read -- `BenchRun.kt`'s own closing delay.
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
sampler_done.store(true, Ordering::Relaxed);
|
||||
if let Some(sampler) = sampler {
|
||||
let _ = sampler.await;
|
||||
}
|
||||
let battery = battery_line(&samples.lock().unwrap());
|
||||
let cpu_line = match (cpu_start, process_cpu_ms()) {
|
||||
(Some(start), Some(end)) => {
|
||||
format!(" process CPU time over this run: {}ms", end.saturating_sub(start))
|
||||
}
|
||||
_ => " process CPU time over this run: unavailable".to_string(),
|
||||
};
|
||||
let rss_line = match peak_rss_kb() {
|
||||
Some(kb) => format!(" peak RSS: {kb}kB"),
|
||||
None => " peak RSS: unavailable (/proc/self/status unreadable)".to_string(),
|
||||
};
|
||||
|
||||
ctx.update(move |state: &mut BenchClient, rsc| {
|
||||
state.running = false;
|
||||
let scroll_line = format!(
|
||||
" scroll: {CYCLES} cycles ({} swipes), streamed {sent}/{total} fixture events",
|
||||
CYCLES * 4
|
||||
);
|
||||
let frames_line = match state.android_state().frame_report.report() {
|
||||
Some(stats) => format!("{stats}"),
|
||||
None => "no frames recorded".to_string(),
|
||||
};
|
||||
let report = format!(
|
||||
"iris bench report\n{frames_line}\n{scroll_line}\n{cpu_line}\n{rss_line}\n{battery}"
|
||||
);
|
||||
log::info!("iris bench report: {report}");
|
||||
state.report_display.edit(rsc).set(&report);
|
||||
state.last_report = Some(report);
|
||||
});
|
||||
redraw.request_redraw();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves `List::scroll` by `total_px` over `duration_ms`, in ~60Hz steps,
|
||||
/// so the swipe is many rendered frames rather than one jump -- the same
|
||||
/// shape `animateScrollBy(SWIPE_PX, tween(SWIPE_MS))` gives on the Compose
|
||||
/// side, in the one place the two backends have to differ (iris's `List`
|
||||
/// has no built-in tween, so this drives it by hand).
|
||||
async fn animate_scroll(
|
||||
ctx: &mut iris::task::TaskCtx<Rsc>,
|
||||
redraw: &Arc<dyn iris::task::RequestRedraw>,
|
||||
total_px: f32,
|
||||
duration_ms: u64,
|
||||
) {
|
||||
let steps = (duration_ms / ANIM_STEP_MS).max(1);
|
||||
let step_px = total_px / steps as f32;
|
||||
for _ in 0..steps {
|
||||
ctx.update(move |state: &mut BenchClient, rsc| {
|
||||
if let Some(screen) = &state.screen {
|
||||
(screen.list)(rsc).scroll(step_px);
|
||||
}
|
||||
});
|
||||
redraw.request_redraw();
|
||||
tokio::time::sleep(Duration::from_millis(ANIM_STEP_MS)).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
//! JNI calls the `bench` feature needs that go through the shell's own
|
||||
//! Java side rather than anything `iris`/`android-view` already wraps:
|
||||
//! `BatteryManager.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW)` for the
|
||||
//! per-second battery sample, and `ClipboardManager.setPrimaryClip` for
|
||||
//! the "Copy report" control (P0's iris half, docs/RUST.md). Neither is
|
||||
//! part of `android_view::context`'s own `Context`/`Resources` wrappers
|
||||
//! (that file's own `// TODO: more methods?`), so this calls them
|
||||
//! directly rather than growing that crate's wrapper for two one-off
|
||||
//! calls this crate alone needs.
|
||||
//!
|
||||
//! Holds its own `JavaVM` + `GlobalRef` to the view (handed in through
|
||||
//! [`iris::android::AndroidAppState::platform_ready`]) so it can attach
|
||||
//! whichever thread calls it -- the battery sampler runs on a background
|
||||
//! tokio task, not the UI thread the rest of `IrisViewPeer`'s JNI calls
|
||||
//! run on. `JavaVM::attach_current_thread` is safe to call from a thread
|
||||
//! already attached (the `jni` crate detects it and does not double
|
||||
//! attach), so no caller here needs to know or care which thread it is.
|
||||
|
||||
use android_view::jni::{
|
||||
JNIEnv, JavaVM,
|
||||
objects::{GlobalRef, JObject, JValue},
|
||||
};
|
||||
|
||||
/// `android.os.BatteryManager.BATTERY_PROPERTY_CURRENT_NOW` -- not exposed
|
||||
/// as a constant anywhere reachable without the Android SDK jar, so named
|
||||
/// here with its source rather than left as a bare `2`.
|
||||
const BATTERY_PROPERTY_CURRENT_NOW: i32 = 2;
|
||||
|
||||
pub struct PlatformHandle {
|
||||
vm: JavaVM,
|
||||
view: GlobalRef,
|
||||
}
|
||||
|
||||
impl PlatformHandle {
|
||||
pub fn new(vm: JavaVM, view: GlobalRef) -> Self {
|
||||
Self { vm, view }
|
||||
}
|
||||
|
||||
fn context<'e>(&self, env: &mut JNIEnv<'e>) -> Option<JObject<'e>> {
|
||||
env.call_method(
|
||||
self.view.as_obj(),
|
||||
"getContext",
|
||||
"()Landroid/content/Context;",
|
||||
&[],
|
||||
)
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn system_service<'e>(
|
||||
&self,
|
||||
env: &mut JNIEnv<'e>,
|
||||
context: &JObject<'e>,
|
||||
name: &str,
|
||||
) -> Option<JObject<'e>> {
|
||||
let jname = env.new_string(name).ok()?;
|
||||
env.call_method(
|
||||
context,
|
||||
"getSystemService",
|
||||
"(Ljava/lang/String;)Ljava/lang/Object;",
|
||||
&[JValue::Object(jname.as_ref())],
|
||||
)
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// One sample of `BATTERY_PROPERTY_CURRENT_NOW`, in microamps. `None`
|
||||
/// on any JNI failure, on a device with no `BatteryManager` service,
|
||||
/// or when the platform itself answers "not supported" -- `0` or
|
||||
/// `Integer.MIN_VALUE` are both documented SDK answers for that, and
|
||||
/// both would read as a real (and wrong) measurement if folded into an
|
||||
/// average rather than named apart. UI_RULES.md: never present an
|
||||
/// inferred value as a measured one.
|
||||
pub fn battery_current_ua(&self) -> Option<i32> {
|
||||
let mut guard = self.vm.attach_current_thread().ok()?;
|
||||
let env: &mut JNIEnv = &mut guard;
|
||||
let context = self.context(env)?;
|
||||
let battery_manager = self.system_service(env, &context, "batterymanager")?;
|
||||
let value = env
|
||||
.call_method(
|
||||
&battery_manager,
|
||||
"getIntProperty",
|
||||
"(I)I",
|
||||
&[JValue::Int(BATTERY_PROPERTY_CURRENT_NOW)],
|
||||
)
|
||||
.ok()?
|
||||
.i()
|
||||
.ok()?;
|
||||
if value == 0 || value == i32::MIN {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts `text` on the system clipboard through `ClipboardManager` --
|
||||
/// `true` only if the whole JNI chain (service lookup, `ClipData`,
|
||||
/// `setPrimaryClip`) succeeded.
|
||||
pub fn copy_to_clipboard(&self, label: &str, text: &str) -> bool {
|
||||
self.try_copy_to_clipboard(label, text).is_some()
|
||||
}
|
||||
|
||||
fn try_copy_to_clipboard(&self, label: &str, text: &str) -> Option<()> {
|
||||
let mut guard = self.vm.attach_current_thread().ok()?;
|
||||
let env: &mut JNIEnv = &mut guard;
|
||||
let context = self.context(env)?;
|
||||
let clipboard = self.system_service(env, &context, "clipboard")?;
|
||||
let jlabel = env.new_string(label).ok()?;
|
||||
let jtext = env.new_string(text).ok()?;
|
||||
let clip = env
|
||||
.call_static_method(
|
||||
"android/content/ClipData",
|
||||
"newPlainText",
|
||||
"(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Landroid/content/ClipData;",
|
||||
&[
|
||||
JValue::Object(jlabel.as_ref()),
|
||||
JValue::Object(jtext.as_ref()),
|
||||
],
|
||||
)
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()?;
|
||||
env.call_method(
|
||||
&clipboard,
|
||||
"setPrimaryClip",
|
||||
"(Landroid/content/ClipData;)V",
|
||||
&[JValue::Object(&clip)],
|
||||
)
|
||||
.ok()?;
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,18 @@
|
||||
//! A build picks one screen or the other, never both, so `Client` and
|
||||
//! `TranscriptClient` are cfg-gated apart rather than switched at runtime --
|
||||
//! there is no in-app navigation to switch *to* on either side yet.
|
||||
//!
|
||||
//! **`bench` feature (P0's iris half, docs/RUST.md):** a third
|
||||
//! `AndroidAppState`, `bench_client::BenchClient`, on the same axis --
|
||||
//! `transcript_ui::build_tree` again, this time against the checked-in
|
||||
//! fixture (`app/bench-fixture/assets/transcript.jsonl`) instead of a real
|
||||
//! server, with a "Run benchmark" control that drives the same scroll loop
|
||||
//! and streaming phase the Compose `bench` build type's `BenchRun.kt`
|
||||
//! does. `bench` depends on `transcript-screen` (Cargo.toml) for
|
||||
//! `transcript-ui`/`client-core`/`event-model`, so both features end up
|
||||
//! enabled together -- `ActiveClient` below gives `bench` priority in that
|
||||
//! case, the same way `transcript-screen` already takes priority over the
|
||||
//! default `tabs-screen`.
|
||||
|
||||
use android_view::{
|
||||
Context, View,
|
||||
@@ -39,7 +51,11 @@ use iris::prelude::*;
|
||||
use log::LevelFilter;
|
||||
use std::ffi::c_void;
|
||||
|
||||
#[cfg(feature = "transcript-screen")]
|
||||
#[cfg(feature = "bench")]
|
||||
mod bench_client;
|
||||
#[cfg(feature = "bench")]
|
||||
mod bench_jni;
|
||||
#[cfg(all(feature = "transcript-screen", not(feature = "bench")))]
|
||||
mod transcript_client;
|
||||
|
||||
/// The app's `View` subclass, matching the Java side's package --
|
||||
@@ -85,8 +101,10 @@ impl AndroidAppState for Client {
|
||||
|
||||
#[cfg(not(feature = "transcript-screen"))]
|
||||
type ActiveClient = Client;
|
||||
#[cfg(feature = "transcript-screen")]
|
||||
#[cfg(all(feature = "transcript-screen", not(feature = "bench")))]
|
||||
type ActiveClient = transcript_client::TranscriptClient;
|
||||
#[cfg(feature = "bench")]
|
||||
type ActiveClient = bench_client::BenchClient;
|
||||
|
||||
extern "system" fn new_view_peer<'local>(
|
||||
env: JNIEnv<'local>,
|
||||
|
||||
@@ -23,6 +23,48 @@ pub use primitive::*;
|
||||
|
||||
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
||||
|
||||
/// The `wgpu::Limits` both platform backends (`android::render::
|
||||
/// AndroidRenderer::new`, `default::render::UiRenderer::new`) ask
|
||||
/// `Adapter::request_device` for -- shared so the two copies cannot drift,
|
||||
/// per AGENTS.md's "write the logic once."
|
||||
///
|
||||
/// Built from `Limits::default()`, **not** a downlevel variant: the shader
|
||||
/// (`shader.wgsl`) reads four `var<storage>` buffers (rects, glyphs, masks,
|
||||
/// move_offsets) from the vertex stage, and `Limits::downlevel_webgl2_defaults()`
|
||||
/// zeroes `max_storage_buffers_per_shader_stage` along with the compute
|
||||
/// limits below -- switching to it would trade one `request_device` crash
|
||||
/// for a bind-group-layout one on the same downlevel hardware this is meant
|
||||
/// to support. `max_buffer_size` is raised for the growing instance/atlas
|
||||
/// buffers (`ArrBuf`, `GpuTextures`); everything else is `default()`'s
|
||||
/// desktop-tier value, unchanged.
|
||||
///
|
||||
/// The six `max_compute_*` fields are zeroed because nothing in this crate
|
||||
/// creates a `ComputePipeline` or writes a `@compute` shader stage --
|
||||
/// grepped for both across `iris`/`iris-core` before writing this, found
|
||||
/// none. `Limits::default()` requests desktop-tier compute limits
|
||||
/// unconditionally (`max_compute_workgroups_per_dimension: 65535`) even
|
||||
/// though nothing asks a device to actually support compute, which is what
|
||||
/// crashed `request_device` on the Android emulator's software GL path
|
||||
/// (`EMU_GPU=software`, `force-gles`): SwiftShader's GL reports itself as
|
||||
/// OpenGL ES 3.0, which has no compute shaders, so the adapter's real limit
|
||||
/// is 0 and the unconditional request fails outright
|
||||
/// (`RUST.md`'s "Software mode ... crashes for a third, different reason").
|
||||
/// The same would happen on a real GLES-3.0-only Android device. If a
|
||||
/// future change adds a compute pass, request the specific limits it needs
|
||||
/// here rather than reverting to the desktop-tier default for everything.
|
||||
pub fn device_limits() -> Limits {
|
||||
Limits {
|
||||
max_buffer_size: 1 << 30,
|
||||
max_compute_workgroup_storage_size: 0,
|
||||
max_compute_invocations_per_workgroup: 0,
|
||||
max_compute_workgroup_size_x: 0,
|
||||
max_compute_workgroup_size_y: 0,
|
||||
max_compute_workgroup_size_z: 0,
|
||||
max_compute_workgroups_per_dimension: 0,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UiRenderNode {
|
||||
uniform_group: BindGroup,
|
||||
primitive_layout: BindGroupLayout,
|
||||
|
||||
@@ -86,12 +86,11 @@ impl AndroidRenderer {
|
||||
|
||||
// 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: Limits {
|
||||
max_buffer_size: 1 << 30,
|
||||
..Default::default()
|
||||
},
|
||||
required_limits: iris_core::device_limits(),
|
||||
..Default::default()
|
||||
})
|
||||
.block_on()
|
||||
|
||||
@@ -4,7 +4,7 @@ use accesskit_android::Adapter as AccessAdapter;
|
||||
use android_view::{
|
||||
AccessibilityNodeInfo, AccessibilityNodeProvider, Bundle, CallbackCtx, Context,
|
||||
InputConnection, KeyEvent, MotionEvent, Rect, View, ViewPeer,
|
||||
jni::{JNIEnv, sys::jint},
|
||||
jni::{JNIEnv, JavaVM, objects::GlobalRef, sys::jint},
|
||||
ndk::event::{Keycode, MotionAction},
|
||||
};
|
||||
// `marker::Sized` explicitly: `crate::prelude::*` below also brings in the
|
||||
@@ -107,6 +107,19 @@ pub trait AndroidAppState: HasAndroidUiState {
|
||||
fn back_pressed(&mut self, rsc: &mut AndroidRsc<Self>, render: &mut UiRenderState) -> bool {
|
||||
false
|
||||
}
|
||||
/// Called once, right after `new`, with a fresh `JavaVM` handle and a
|
||||
/// global reference to this app's own `View` -- for a caller that
|
||||
/// needs to call into Java itself beyond what a [`RequestRedraw`]
|
||||
/// handle already covers (P0's bench build calling
|
||||
/// `BatteryManager`/`ClipboardManager` through the view's `Context`,
|
||||
/// docs/RUST.md). Not folded into `new` itself: most implementors need
|
||||
/// nothing here, and `new`'s job is building the widget tree, not
|
||||
/// holding a platform handle -- the default does nothing. `vm`/`view`
|
||||
/// are independent handles from the ones `new_peer` keeps for its own
|
||||
/// `RequestRedraw` (a fresh `get_java_vm`/`new_global_ref` each), so
|
||||
/// storing them has no effect on that mechanism.
|
||||
#[allow(unused_variables)]
|
||||
fn platform_ready(&mut self, rsc: &mut AndroidRsc<Self>, vm: JavaVM, view: GlobalRef) {}
|
||||
}
|
||||
|
||||
/// The android-view analogue of `default::DefaultRsc` -- identical in
|
||||
@@ -561,7 +574,10 @@ pub fn new_peer<'local, State: AndroidAppState>(
|
||||
};
|
||||
let shared = Rc::new(RefCell::new(Shared::default()));
|
||||
let ui_state = AndroidUiState::new(shared.clone());
|
||||
let state = State::new(ui_state, &mut rsc);
|
||||
let mut state = State::new(ui_state, &mut rsc);
|
||||
let platform_vm = env.get_java_vm().unwrap();
|
||||
let platform_view = env.new_global_ref(&view.0).unwrap();
|
||||
state.platform_ready(&mut rsc, platform_vm, platform_view);
|
||||
let peer = IrisViewPeer {
|
||||
rsc,
|
||||
render: UiRenderState::new(),
|
||||
|
||||
@@ -102,13 +102,12 @@ impl UiRenderer {
|
||||
// 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.
|
||||
// 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: Limits {
|
||||
max_buffer_size: 1 << 30,
|
||||
..Default::default()
|
||||
},
|
||||
required_limits: iris_core::device_limits(),
|
||||
..Default::default()
|
||||
})
|
||||
.block_on()
|
||||
|
||||
@@ -35,6 +35,34 @@ fn iris_features() -> Features {
|
||||
/// kept for the big storage buffers behind rects/glyphs.
|
||||
const IRIS_MAX_BUFFER_SIZE: u64 = 1 << 30;
|
||||
|
||||
/// Mirrors `iris_core::device_limits()` (`iris/core/src/render/mod.rs`) --
|
||||
/// cannot call it directly, since this rig is deliberately its own crate,
|
||||
/// not a workspace member (this file's own Cargo.toml comment). Keep the
|
||||
/// two in sync by hand when one changes; this rig's whole purpose is "does
|
||||
/// the device iris actually builds come back," so a stale copy here would
|
||||
/// silently stop answering that question. Zeroed rather than left at
|
||||
/// `Limits::default()`'s desktop-tier values because nothing in iris
|
||||
/// creates a `ComputePipeline` or a `@compute` shader stage -- found by
|
||||
/// grepping the whole `iris`/`iris-core` tree before this rig's comment was
|
||||
/// written -- and the unconditional default request is what crashed
|
||||
/// `request_device` on the Android emulator's software GL path
|
||||
/// (`EMU_GPU=software`, `force-gles`: SwiftShader's GL reports itself as
|
||||
/// OpenGL ES 3.0, which has no compute shaders at all, so the adapter's
|
||||
/// real limit is 0). The same would happen on a real GLES-3.0-only Android
|
||||
/// device.
|
||||
fn iris_limits() -> Limits {
|
||||
Limits {
|
||||
max_buffer_size: IRIS_MAX_BUFFER_SIZE,
|
||||
max_compute_workgroup_storage_size: 0,
|
||||
max_compute_invocations_per_workgroup: 0,
|
||||
max_compute_workgroup_size_x: 0,
|
||||
max_compute_workgroup_size_y: 0,
|
||||
max_compute_workgroup_size_z: 0,
|
||||
max_compute_workgroups_per_dimension: 0,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
vk::report();
|
||||
|
||||
@@ -104,10 +132,7 @@ fn main() {
|
||||
// limits requested, this is expected to succeed everywhere -- this rig
|
||||
// is what turned that from an assumption into a measurement, first on
|
||||
// this emulator's software Vulkan.
|
||||
let wanted = Limits {
|
||||
max_buffer_size: IRIS_MAX_BUFFER_SIZE,
|
||||
..Default::default()
|
||||
};
|
||||
let wanted = iris_limits();
|
||||
match pollster::block_on(adapter.request_device(&DeviceDescriptor {
|
||||
required_features: iris_features(),
|
||||
required_limits: wanted,
|
||||
|
||||
Reference in new issue
Block a user