Initial workspace: GPU context, compute pass, adaptive Slint shell

Establishes the v0.1 foundations on both platforms:

- dr-types: SourceRef (never a filesystem path — Android SAF has none),
  Format, Availability, Validator with ETag quote normalisation
- dr-gpu: wgpu device, compute pass writing a storage texture, resize
- dr-ui: Slint shell with FR-UI-1 adaptive layout, computed in Rust to
  avoid a binding loop
- docker/android: pinned toolchain, verified producing API 28 ARM binaries

Measured the cost of the temporary CPU readback path (dr-gpu bench):
compute is 0.06-0.28ms across sizes while readback is 0.63-7.43ms, so
readback is 90-96% of frame time and scales with area. Recorded in
ARCH §6.1 — this is why spike S1 is the priority.

Mitigations pending S1: reuse the staging buffer, apply at most one
resize per frame, and cap render resolution at 2048 on the long edge.

10 tests passing; core crates cross-compile for aarch64-linux-android.
This commit is contained in:
2026-08-09 07:42:05 +02:00
commit 82a5e21ec6
25 changed files with 10707 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
// Measures the per-frame cost at several canvas sizes, isolating the readback
// path from windowing.
use dr_gpu::{GpuContext, RenderTarget};
use std::time::Instant;
fn main() {
env_logger::init();
let ctx = pollster::block_on(GpuContext::new_headless()).unwrap();
println!("adapter: {}\n", ctx.adapter_name());
println!("{:>12} {:>10} {:>10} {:>8}", "size", "compute", "+readback", "fps");
for &(w, h) in &[(840u32, 692u32), (1280, 720), (1920, 1080), (2048, 1152), (3840, 2160)] {
let rt = RenderTarget::new(&ctx, w, h).unwrap();
// warm
for i in 0..10 { rt.render(i as f32 * 0.01); let _ = pollster::block_on(rt.read_pixels()); }
let n = 40;
let t0 = Instant::now();
for i in 0..n { rt.render(i as f32 * 0.01); }
ctx.device.poll(wgpu::Maintain::Wait);
let compute = t0.elapsed().as_secs_f64() / n as f64;
let t1 = Instant::now();
for i in 0..n {
rt.render(i as f32 * 0.01);
let _ = pollster::block_on(rt.read_pixels()).unwrap();
}
let full = t1.elapsed().as_secs_f64() / n as f64;
println!("{:>5}x{:<6} {:>8.2}ms {:>8.2}ms {:>8.0}",
w, h, compute * 1000.0, full * 1000.0, 1.0 / full);
}
}