Items 1-3 of Iris's 22:16 phone report, plus the two defects that were hiding behind item 1 and only became visible once the first one was fixed. Emulator evidence and the numbers are in docs/RUST.md. **Keyboard reopen.** `attr.rs`'s already-focused branch calls `focus_gained` on a tap that stays inside `DRAG_SLOP` -- what Android's own `EditText` does, `showSoftInput` being idempotent. Dismissing the IME leaves the field focused, so the only branch that requested it never ran again. Negative control run: without this one call the second tap leaves `mInputShown=false`. Swipes across and out of the focused field still summon nothing. **IME height.** `MainActivity` sends `getInsets(ime()).bottom` and `isVisible(ime())` as two values; the height used to be sent *as* the boolean, so nothing had a number to pad by. `Insets`/`WindowInsets` carry both, `bench_client` reads the boolean for its state machine and the height for `Composer::set_bottom_inset`, and the list follows because it is `rest(1)` in the same `Span`. **Fling.** Three defects, in the order they were found: 1. `on_touch_event` read only each `MotionEvent`'s final position, so a batched 120Hz flick fed the tracker one sample and `velocity()` answered 0.0. Historical samples are replayed through the sensor pass now, `CursorState::time` carries each sample's own time (so a replay loop's speed cannot become the measured velocity -- the winit backend sets it too), the press is a sample as AOSP's own tracker does, and `iris drag release:` logs the decision for the phone's logcat. 2. Nothing advanced a fling between input events: `tick_fling`'s only caller was the benchmark's own loop, so the bench flung and a finger never did. iris has one animation mechanism now -- `Widget::tick`, `UiData::animate`/`tick_animations`, called by both backends before the draw and re-requesting a frame while it answers true. 3. With flings finally animating, one lasted 45 seconds: `List::fling` hardcoded density 1.0 against physical-pixel velocities, and `FlingCalculator`'s coefficient used the scroll friction where AOSP uses its 0.84 tuning constant -- 56x, inside an exponential. Emulator: 1.62s for v=11064, against AOSP's own 1.586s. **Two pre-existing faults found on the way.** `MOVE_CHAIN_LIMIT` was 16 and the composer's chain is 17, so every debug build aborted on a tap of the composer and every release build silently drew and hit-tested that subtree short; it is 64 in both the CPU walk and shader.wgsl, and the assert prints the chain so a cycle and a deep tree can be told apart. And `minSdk` is 29, since `getEventTimeNanos` is API 29 and a missing JNI method is a crash rather than a degraded fling. Every new invariant carries its guard: sample times non-decreasing in `on_touch_event`, and tests confirmed to fail without their fix for the press-seeded velocity, the animation registration and the AOSP magnitudes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
257 lines
7.7 KiB
WebGPU Shading Language
257 lines
7.7 KiB
WebGPU Shading Language
const RECT: u32 = 0u;
|
|
// TEXTURE has no entry in group 1: a standalone image draws with its own
|
|
// bind group (see UiRenderNode::draw), so there is nothing per-instance left
|
|
// to look up here -- the bind group already picked the texture.
|
|
const TEXTURE: u32 = 1u;
|
|
const GLYPH: u32 = 2u;
|
|
|
|
@group(0) @binding(0)
|
|
var<uniform> window: WindowUniform;
|
|
@group(1) @binding(RECT)
|
|
var<storage> rects: array<Rect>;
|
|
@group(1) @binding(GLYPH)
|
|
var<storage> glyphs: array<GlyphInfo>;
|
|
|
|
struct Rect {
|
|
color: u32,
|
|
radius: f32,
|
|
thickness: f32,
|
|
inner_radius: f32,
|
|
}
|
|
|
|
struct GlyphInfo {
|
|
uv_min: vec2<f32>,
|
|
uv_max: vec2<f32>,
|
|
// Layer of the shared atlas array texture, not a view or bind-group
|
|
// index -- a page never gets its own bind group. See TEXTURES.md's
|
|
// "Recommended shape".
|
|
layer: u32,
|
|
color: u32,
|
|
flags: u32,
|
|
}
|
|
|
|
struct Mask {
|
|
x: UiSpan,
|
|
y: UiSpan,
|
|
move_idx: u32,
|
|
}
|
|
|
|
/// One widget's cumulative on-screen translation and the slot of the
|
|
/// ancestor to add on top of it. Mirrors `MoveOffset` in data.rs.
|
|
struct MoveOffset {
|
|
delta: vec2<f32>,
|
|
parent: u32,
|
|
}
|
|
|
|
struct UiSpan {
|
|
start: UiScalar,
|
|
end: UiScalar,
|
|
}
|
|
|
|
struct UiScalar {
|
|
rel: f32,
|
|
abs: f32,
|
|
}
|
|
|
|
struct UiVec2 {
|
|
rel: vec2<f32>,
|
|
abs: vec2<f32>,
|
|
}
|
|
|
|
// The shared glyph atlas: every page is one layer. Growing it recreates this
|
|
// texture with headroom and copies the old layers across -- see
|
|
// GpuTextures::grow_array -- rather than the binding_array<texture_2d<f32>>
|
|
// this replaced, which needed VK_EXT_descriptor_indexing and does not survive
|
|
// a real share of Android GPUs (see TEXTURES.md).
|
|
@group(2) @binding(0)
|
|
var atlas: texture_2d_array<f32>;
|
|
// One standalone image's texture. The main draw (rects and glyphs) binds a
|
|
// 1x1 null texture here, since neither samples it; each image draw call
|
|
// binds its own -- see UiRenderNode::draw.
|
|
@group(2) @binding(1)
|
|
var image_texture: texture_2d<f32>;
|
|
@group(2) @binding(2)
|
|
var samp: sampler;
|
|
// Their own group, bound once per frame rather than folded into group 2: see
|
|
// UiRenderNode::masks_layout for why an image's own bind group must not name
|
|
// either buffer.
|
|
@group(3) @binding(0)
|
|
var<storage> masks: array<Mask>;
|
|
@group(3) @binding(1)
|
|
var<storage> move_offsets: array<MoveOffset>;
|
|
|
|
// The bound on the parent walk, kept in step with `MOVE_CHAIN_LIMIT` in
|
|
// render_state.rs, which walks the identical chain on the CPU side for
|
|
// hit-testing. Bounded so a malformed chain (a cyclic `parent`) cannot
|
|
// hang the GPU -- not a claim about how deep a real tree gets. It was 16
|
|
// and that was too small: the transcript screen's composer field sits 17
|
|
// slots below the root, measured 2026-09-07 on this checkout's emulator
|
|
// by tapping it (the CPU walk's own debug assert names the chain now).
|
|
// Past the bound both walks simply stop summing, so the widget draws and
|
|
// hit-tests short by whatever the outer slots held, with nothing on
|
|
// screen to say so.
|
|
const MOVE_CHAIN_LIMIT: u32 = 64u;
|
|
|
|
/// Sums the pixel delta along the parent chain starting at `idx`, shared by
|
|
/// the vertex stage (a primitive's own corners) and the fragment stage (its
|
|
/// mask's corners) so the walk is written once. See LAYOUT.md section 2b.
|
|
fn resolve_move(idx: u32) -> vec2<f32> {
|
|
var total = vec2<f32>(0.0, 0.0);
|
|
var i = idx;
|
|
for (var step = 0u; step < MOVE_CHAIN_LIMIT; step++) {
|
|
let entry = move_offsets[i];
|
|
total += entry.delta;
|
|
if entry.parent == 4294967295u {
|
|
break;
|
|
}
|
|
i = entry.parent;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
struct WindowUniform {
|
|
dim: vec2<f32>,
|
|
};
|
|
|
|
struct InstanceInput {
|
|
@location(0) x_start: vec2<f32>,
|
|
@location(1) x_end: vec2<f32>,
|
|
@location(2) y_start: vec2<f32>,
|
|
@location(3) y_end: vec2<f32>,
|
|
@location(4) binding: u32,
|
|
@location(5) idx: u32,
|
|
@location(6) mask_idx: u32,
|
|
@location(7) move_idx: u32,
|
|
}
|
|
|
|
struct VertexOutput {
|
|
@location(0) top_left: vec2<f32>,
|
|
@location(1) bot_right: vec2<f32>,
|
|
@location(2) uv: vec2<f32>,
|
|
@location(3) binding: u32,
|
|
@location(4) idx: u32,
|
|
@location(5) mask_idx: u32,
|
|
@builtin(position) clip_position: vec4<f32>,
|
|
};
|
|
|
|
struct Region {
|
|
pos: vec2<f32>,
|
|
uv: vec2<f32>,
|
|
top_left: vec2<f32>,
|
|
bot_right: vec2<f32>,
|
|
}
|
|
|
|
@vertex
|
|
fn vs_main(
|
|
@builtin(vertex_index) vi: u32,
|
|
in: InstanceInput,
|
|
) -> VertexOutput {
|
|
var out: VertexOutput;
|
|
|
|
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
|
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
|
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
|
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
|
|
|
|
let move_delta = resolve_move(in.move_idx);
|
|
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs) + move_delta;
|
|
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs) + move_delta;
|
|
let size = bot_right - top_left;
|
|
|
|
let uv = vec2<f32>(
|
|
f32(vi % 2u),
|
|
f32(vi / 2u)
|
|
);
|
|
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
|
|
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
|
out.uv = uv;
|
|
out.binding = in.binding;
|
|
out.idx = in.idx;
|
|
out.top_left = top_left;
|
|
out.bot_right = bot_right;
|
|
out.mask_idx = in.mask_idx;
|
|
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_main(
|
|
in: VertexOutput
|
|
) -> @location(0) vec4<f32> {
|
|
let pos = in.clip_position.xy;
|
|
let region = Region(pos, in.uv, in.top_left, in.bot_right);
|
|
let i = in.idx;
|
|
var color: vec4<f32>;
|
|
switch in.binding {
|
|
case RECT: {
|
|
color = draw_rounded_rect(region, rects[i]);
|
|
}
|
|
case TEXTURE: {
|
|
color = draw_texture(region);
|
|
}
|
|
case GLYPH: {
|
|
color = draw_glyph(region, glyphs[i]);
|
|
}
|
|
default: {
|
|
color = vec4(1.0, 0.0, 1.0, 1.0);
|
|
}
|
|
}
|
|
if in.mask_idx != 4294967295u {
|
|
let mask = masks[in.mask_idx];
|
|
let mask_delta = resolve_move(mask.move_idx);
|
|
let tl = UiVec2(vec2(mask.x.start.rel, mask.y.start.rel), vec2(mask.x.start.abs, mask.y.start.abs));
|
|
let br = UiVec2(vec2(mask.x.end.rel, mask.y.end.rel), vec2(mask.x.end.abs, mask.y.end.abs));
|
|
|
|
let top_left = floor(tl.rel * window.dim) + floor(tl.abs) + mask_delta;
|
|
let bot_right = floor(br.rel * window.dim) + floor(br.abs) + mask_delta;
|
|
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
|
color *= 0.0;
|
|
}
|
|
}
|
|
return color;
|
|
}
|
|
|
|
fn draw_texture(region: Region) -> vec4<f32> {
|
|
return textureSample(image_texture, samp, region.uv);
|
|
}
|
|
|
|
fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
|
|
let uv = mix(g.uv_min, g.uv_max, region.uv);
|
|
let texel = textureSample(atlas, samp, uv, i32(g.layer));
|
|
if (g.flags & 1u) != 0u {
|
|
return texel;
|
|
}
|
|
var color = unpack4x8unorm(g.color);
|
|
color.a *= texel.a;
|
|
return color;
|
|
}
|
|
|
|
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
|
|
var color = unpack4x8unorm(rect.color);
|
|
|
|
let edge = 0.5;
|
|
|
|
let size = region.bot_right - region.top_left;
|
|
let corner = size / 2.0;
|
|
let center = region.top_left + corner;
|
|
|
|
let dist = distance_from_rect(region.pos, center, corner, rect.radius);
|
|
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
|
|
|
|
if rect.thickness > 0.0 {
|
|
let dist2 = distance_from_rect(region.pos, center, corner - rect.thickness, rect.inner_radius);
|
|
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
|
|
}
|
|
|
|
return color;
|
|
}
|
|
|
|
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
|
|
// vec from center to pixel
|
|
let p = pixel_pos - rect_center;
|
|
// vec from inner rect corner to pixel
|
|
let q = abs(p) - (rect_corner - radius);
|
|
return length(max(q, vec2(0.0))) - radius;
|
|
}
|
|
|