From 4c7072e62fb247bce8c5e1f1ceeee98dac1b97c4 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Sun, 20 Sep 2026 16:50:50 -0400 Subject: [PATCH] Document deferred layout requests and verification on Iris PR 19 --- docs/HANDOFF.md | 52 +++++++++++++++++++++++++++++------------ docs/LAYOUT.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/PLAN.md | 7 ++++-- 3 files changed, 104 insertions(+), 16 deletions(-) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index e42aed7..ff86754 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -4,10 +4,33 @@ Where the work in flight stands. The settled layout design, the vocabulary and the measurement method are in `docs/LAYOUT.md`; what the review of #19 found is in `docs/LAYOUT_LOG.md`. +## Deferred comparisons are submitted on PR #19 + +The `layout/one-ask` tip is `8780b40`, in `/home/bob/repos/iris-deferred`. +`/home/bob/repos/iris` remains on `layout/bounds`; that branch's `de1eb7e` was +fast-forwarded into #19 before this work. The app's Iris pin is untouched. + +The new request path composes min/max/clamp before assigning leftover slots, +while ordinary widgets keep returning `Size`. Known requests skip provisional +painting; measured leaves can complete a nested expression. The implementation +uses a reusable expression arena and retained draw buffers. The allocation rig +checks zero allocations after warm-up for unchanged plain and clamped trees. +Generalized cross-axis maxima are deferred because hidden main-axis shares +must not contribute to them. The current design is in `docs/LAYOUT.md`. + +Format, workspace Clippy with and without layout diagnostics, and all 197 +workspace tests pass. The broad scans passed: 400 depth-5 trees through the +shrinker, 1,000 depth-6 and 2,000 depth-4 generated trees, plus 400 depth-5 +expression trees and 400 relative-bound trees. All 34,986 unbounded cold boxes +match `de1eb7e`. The forced-redraw allocation fixtures report zero allocations +across 100 resize frames after warm-up. The text resize benchmark uses 5.89% +more retired instructions than `de1eb7e`; this is not a universal speedup. +See `docs/LAYOUT.md` for the measurements and remaining measured paths. + ## The Iris layout repair is submitted -**Iris PR #19** (`layout/one-ask`) replaces closed #18. The tip is `2dba90b`, -and past the reviewed `cadfba0` it is twelve rounds, each described in +**Iris PR #19** (`layout/one-ask`) replaces closed #18. Before the deferred +comparison work above, its review rounds past `cadfba0` are described in `docs/LAYOUT_LOG.md`: - **The repair**, `add6774` and `84dad21` -- collapsed-share placement, @@ -68,9 +91,8 @@ over 2026-09-17 to 19; it is current, not frozen. The core design remains sound. Round-to-nearest is still unchanged. -Two checkouts share one Git storage: `/home/bob/repos/iris` is the active -`layout/one-ask` worktree, and `ai-app-2/iris` stays on `app-pin` at the -app's `32f6ad8` pin until the integration below is ready. +The Iris worktrees share Git storage. `ai-app-2/iris` stays on `app-pin` at +the app's `32f6ad8` pin until the integration below is ready. ### The branch layout, and the trap that used to be here @@ -198,17 +220,19 @@ glyphs do not follow a shortened entry. are caller bugs under `debug_assert`, but the fallbacks differ. - `docs/LAYOUT.md` §4, §5 and the density section name `Painter::place`, `Painter::region()`, `SetSize`, `desired_width`, `apply_rest`, `Len::dp`, - `Aligned` and `MaxSize`, none of which exist. Do not restore - `OnResize::Translate` or `OrthoSize`. -- **`layout/bounds` (`de1eb7e`) is green and ready to review**, kept off #19 - so that branch stays one subject. A rule holds what a widget answers + `Aligned`, which no longer exist; `MaxSize` now exists with the bounds API. + Do not restore `OnResize::Translate` or `OrthoSize`. +- **`layout/bounds` (`de1eb7e`) is included in #19** for the deferred + comparison implementation. A rule holds what a widget answers (`SizeRule::{Min, Max, Clamp}`); a widget holds the box (`MaxSize`, which `.max_width`/`.max_height` build). Bryan settled the split on 2026-09-20 - after four readings were measured; the reasoning and the one remaining hole - -- a retained answer re-placed under a different rel base, which is older - than bounds -- are in `docs/LAYOUT_LOG.md`. A bound may not contain - `leftover`, and the generated trees grow bounds in pixels until that hole is - closed. + after four readings were measured; the reasoning is in `docs/LAYOUT_LOG.md`. + The deferred-request work fixes stale relative-bound reuse by tracking the + input base and comparing resolved bounds, with a reduced natural-size-hint + regression and 400 depth-5 relative-bound trees. Intrinsic bounds remain + `Len`s; comparisons involving leftover use `SizeRequest`. Ordinary generated + trees retain pixel bounds; `deferred_generated` supplies relative bounds and + expressions in separate corpora. - `LazySpan`. - `Scroll` taking a direction rather than one axis. diff --git a/docs/LAYOUT.md b/docs/LAYOUT.md index 81d71f7..64abc97 100644 --- a/docs/LAYOUT.md +++ b/docs/LAYOUT.md @@ -12,6 +12,67 @@ in flight stands. The sections from "Three names, and the one argument that says them" onwards are the settled design, the findings that outlived the working log, and the measurement method. +## Deferred size requests on PR #19 + +`Widget::draw` still returns the small, copyable `Size` of two `LayoutLen`s. +An optional `size_request` method describes an axis without drawing it. Its +context can query children and compose sums, minima and maxima; `None` means +that drawing in a concrete offer is necessary. Existing leaf widgets need no +new method: their `size_hint` supplies a plain request when available. + +Declarations can use expressions, for example +`rect(color).width(leftover(1).clamp(40, 120))`. In a 300-pixel row beside an +uncapped equal share, that child takes 120 and its sibling takes 180. At 100 +pixels they take 50 each. Comparisons can have shares on both sides, such as +`(px(30) + leftover(1)).min(leftover(2))`. Weights must be nonnegative. + +A span discovers requests, measures any unknown content, solves the shared +allocation, then places children. Known requests avoid provisional painting. +After measuring a container, discovery can use its children's measured answers +to finish an expression that was previously incomplete. Thus a measured leaf +inside a nested capped row does not force the row's request to become a pixel +constant. A custom container must implement request composition to propagate +such expressions through itself. + +Plain sums retain the existing symbolic allocation path. Comparisons with a +known ordering (pixels with equal fractional/share coefficients, or shares +with equal fixed terms) fold to ordinary lengths. Remaining expressions are +monotone piecewise linear functions of one share unit. The allocator advances +through exact rational crossings until the sum fills the offered room. Caps +return room to other shares; floors may overflow; all caps being reached may +leave unused room. Prefix rounding gives adjacent slots the same edge. + +Persistent declarations share immutable expression nodes. Temporary composition +uses a reusable arena, and each retained drawing keeps its vector capacities. +Temporary request handles are valid only within that layout pass. An explicit +allocated placement distinguishes a solved slot from a box that merely fills +its parent's region, so declarations are not evaluated a second time against +the slot they already chose. Request dependencies invalidate the allocator when +a descendant's rules or contents change. A solved declaration retains its +pixel length when moved, including by scrolling; it must not become a fresh +fraction of the destination box. Relative intrinsic bounds record their input +base separately from the widget's resulting box, and retained answers compare +resolved bounds before reuse. + +Two measured cases remain intentional. Intrinsic fixed content is drawn in the +remaining offer and then moved; drawing it in its reported size would change +wrapping and overflow. When no leftover room exists, discovered intrinsic +shares use that same measured path. Cross-axis span maxima also remain measured: +a main-axis share can be hidden, so the maximum of every child's request is +not necessarily the maximum of the children that actually draw. Generalizing +that would require carrying visibility through the expression system too. + +The allocation rig forces every widget to redraw across 100 resize frames: +both the plain and clamped eight-row fixtures allocate zero times after warm-up. +This is a measured property of those stable visible trees, not a guarantee for +arbitrary widgets or visibility changes. An eight-level known nested span paints +its leaf once. The 40-row, 1,000-frame text resize rig costs 9,891,085,554 retired +instructions versus 9,341,015,256 at `de1eb7e` (5.89% more), with the same printed +geometry. Request dependency tracking has a cost even when text still needs +measurement. The 400-tree unbounded cold dump preserves all 34,986 boxes. + +The app's submodule pin is unchanged. This design lives on Iris PR #19. + ## Design ### UI ownership and frame access diff --git a/docs/PLAN.md b/docs/PLAN.md index be28774..66c1ef5 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -870,8 +870,11 @@ span rather than silently discarding its `rest` component. the box a parent asks a widget in is its *region*, where the drawing ends up is its *placement*, and the length a fraction resolves against is its *rel base*. A container says all three in one `PlaceDesc` argument. `Widget::draw` -remains the only layout body. The invariants, the measured costs, the fuzzing -method and the failed hypotheses are all in `docs/LAYOUT.md`; the app's pin at +remains the only layout body. PR #19 also has optional size-request discovery +for deferred min/max/clamp expressions (2026-09-20); ordinary widgets still +return `Size`, and plain requests retain symbolic allocation. The invariants, +the measured costs, the fuzzing method and the failed hypotheses are all in +`docs/LAYOUT.md`; the app's pin at `32f6ad8` predates every part of it, so nothing here depends on it until the integration described in `docs/HANDOFF.md` lands.