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
+166
View File
@@ -0,0 +1,166 @@
import { Theme } from "theme.slint";
// Status strip — surfaces the GPU backend and adapter, which matters during
// v0.1 because assumption A1 is exactly "does this compositing path work on
// this hardware". Seeing the adapter at a glance makes vendor differences
// obvious during the S1/S2 spikes.
component StatusBar inherits Rectangle {
in property <string> adapter;
in property <string> backend;
in property <string> layout-class;
in property <int> fps;
height: 28px;
background: Theme.surface;
HorizontalLayout {
padding-left: Theme.gap;
padding-right: Theme.gap;
spacing: Theme.gap;
alignment: start;
Text {
text: root.backend;
color: Theme.accent;
font-size: Theme.text-sm;
font-weight: 700;
vertical-alignment: center;
}
Text {
text: root.adapter;
color: Theme.ink-dim;
font-size: Theme.text-sm;
vertical-alignment: center;
overflow: elide;
}
Rectangle { horizontal-stretch: 1; }
Text {
text: root.layout-class;
color: Theme.ink-faint;
font-size: Theme.text-sm;
vertical-alignment: center;
}
Text {
text: root.fps + " fps";
color: root.fps >= 55 ? Theme.ink-dim : Theme.accent;
font-size: Theme.text-sm;
vertical-alignment: center;
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: Theme.rule;
}
}
// A placeholder panel standing in for the adjustment controls that FR-DEV-3a
// will generate from operation descriptors.
component SidePanel inherits Rectangle {
background: Theme.surface;
VerticalLayout {
padding: Theme.gap;
spacing: Theme.gap;
alignment: start;
Text {
text: "DEVELOP";
color: Theme.accent;
font-size: Theme.text-sm;
font-weight: 700;
letter-spacing: 1.2px;
}
Text {
text: "Controls are generated from operation\ndescriptors in v0.2 (FR-DEV-3a).";
color: Theme.ink-faint;
font-size: Theme.text-sm;
wrap: word-wrap;
}
}
Rectangle {
width: 1px;
background: Theme.rule;
}
}
export component AppWindow inherits Window {
title: "DarkRoom";
background: Theme.ground;
preferred-width: 1100px;
preferred-height: 720px;
min-width: 360px;
min-height: 320px;
// Set from Rust each frame: the compute output, delivered as a texture.
in property <image> canvas;
in property <string> adapter: "detecting…";
in property <string> backend: "—";
in property <int> fps: 0;
// FR-UI-1: layout class follows window width, not device type. A narrow
// desktop window gets the compact layout, exactly as a tablet would.
//
// Set from Rust rather than derived from `root.width` here: a property
// read inside the layout and also feeding it creates a binding loop,
// which Slint warns about and which can panic at runtime.
in property <bool> expanded: true;
in property <string> layout-class: "expanded";
callback canvas-resized(int, int);
callback window-resized(length);
// One-way: report width outward, never read layout back into it.
changed width => { root.window-resized(self.width); }
VerticalLayout {
StatusBar {
adapter: root.adapter;
backend: root.backend;
layout-class: root.layout-class;
fps: root.fps;
}
HorizontalLayout {
// The canvas: compute output composited directly. No CPU
// round-trip anywhere in this path (ARCH §6.1).
canvas-area := Rectangle {
horizontal-stretch: 1;
background: Theme.ground;
clip: true;
Image {
width: 100%;
height: 100%;
source: root.canvas;
image-fit: contain;
}
// Report size changes so the render target can be resized to
// match. Width and height are tracked separately because
// Slint has no single "geometry changed" hook.
property <int> px-w: Math.round(self.width / 1px);
property <int> px-h: Math.round(self.height / 1px);
changed px-w => { root.canvas-resized(self.px-w, self.px-h); }
changed px-h => { root.canvas-resized(self.px-w, self.px-h); }
}
// Always instantiated, width collapsed to zero in compact mode.
// A conditional `if` here creates a binding loop — the layout
// depends on `expanded`, which derives from the window width,
// which the layout then influences. Slint flags it, and it can
// panic at runtime.
SidePanel {
width: root.expanded ? 260px : 0px;
visible: root.expanded;
}
}
}
}