FluentGpu
September 12, 2026 · View on GitHub
Read this first when two docs disagree. The design set is large (26 docs, ~16k lines) and several docs each call themselves "authoritative" / "design-of-record". This file is the single precedence map: for every cross-cutting contract it names the one owning doc and states the current canonical value inline, so you never have to discover a supersession by reading three docs.
All of
core-fundamentals-gap-analysis.md(Tier-1..Tier-3) is now folded into CORE — there is no v2/deferred carve-out for any gap there; each is a fully-specified, buildable core design in its owning subsystem doc. The only residuals are the genuinely physical limits (a sustained GPU stall still bounds back to the UI thread; production safety == CI coverage; the build order ships single-thread- correct first, then flips parallelism behind the race gate) — truths, not gaps.Drift is gated:
check-canon.ps1fails the build if a known-stale token reappears in the live tree. Last reconciled against the docs: 2026-08-21.
1. Precedence (tie-breaker for contracts not in the table below)
Each contract in §2 has exactly one owner — that owner wins, regardless of generic order. For anything not listed, resolve conflicts in this order (higher wins):
SPEC-INDEX.md(this file) — the canonical value.hardened-v1-plan.md— threading, safety posture, build order. Its §7 amendment checklist is canonical even wherearchitecture-spec.md/foundations.mdstill print the lean single-thread form (those carry a⊳ Canonical threading modelbanner pointing here).dotnet10-csharp14-zero-alloc.md§4 — the COM ruling.- Subsystem design-of-record docs (
com-interop,validation,scene-memory,dsl-aot,gpu-renderer,reconciler-hooks,layout,text,pal-rhi,input-a11y,media-pipeline,theming,virtualization,backdrop-effects-animation,window-backdrop-mica,threading-render-seam,controls,devtools) — deep design within the rulings above. architecture-spec.md— the end-to-end integrating narrative; canonical owner of the handle byte layout, the color/DPI contract, and the frame-phase shape.foundations.md— the shared vocabulary. Where it conflicts with anything above, the above wins.README.md— the digest. Never authoritative over a deeper doc.
2. Canonical contracts
| Contract | Canonical owner | Current canonical value |
|---|---|---|
| UI-owned cold memory maintenance | subsystems/threading-render-seam.md §0 | Coalesced monotonic one-shot deadlines bound the existing UI wait and drain before minimized/idle early-outs, outside protected paint. Retention or scene allocation/free changes arm work; unsuccessful attempts do not poll. No frame request, publication, present credit or GPU work is created. Snapshot replacement still requires a real Free-to-Writing claim. |
| Isolated keyed child planning | subsystems/reconciler-hooks.md §0bis | ChildReconcilePlan owns input references/handles and resumable pure indexing/matching. One validated IChildReconcileCommitter adapter applies a complete plan. Large child diffs use reusable per-recursion plans, currently drained synchronously before an indivisible commit. This is not hosted staged mounting or rollback of arbitrary callbacks. |
| Hosted scene publication and independent recording | subsystems/threading-render-seam.md §0 | UI publishes detached SceneRecordingSnapshot inputs; renderer owns SceneRecordingContext, command arenas and supported compositor overlays. Three generation/phase-stamped slots claim before reading and retain the current consumer slot across animation-only turns. Presented feedback is reverse-published before the next UI input turn. Snapshot resource pins and actual completed GPU fences independently protect CPU and GPU lifetime. |
| Independent render display-clock subscription | subsystems/pal-rhi.md §1.1.1 | IPlatformWindow.CreateRenderDisplayClock() optionally creates one IRenderDisplayClock for the render consumer. Its auto-reset tick and active subscription are independent of UI pacing; Win32 shares the existing compositor waiter, not its UI event. Capability failure wakes the consumer to select a bounded fallback. |
| Reactive scheduler deadlines | subsystems/reconciler-hooks.md §0bis | ReactiveRuntime.Flush(long deadlineTicks) takes one absolute Stopwatch deadline shared by all reflushes in a UI turn and returns ReactiveFlushResult. It yields between complete computation invocations, retains unread queue cursors and dedup flags, and prioritizes structural work before the next normal unit. Runaway accounting persists until quiescence, including across yielded frames. This does not make the live-mutating reconciler transactional or preempt an individual callback. |
| Handle layout | architecture-spec.md §4.1 (mirrored in foundations.md §1.1, scene-memory.md §1.1) | Handle = 8 bytes {u32 Index, u32 Gen}. Generation is 32-bit, bumped on alloc and free (ABA defense). Kind is NOT packed into the bits — it lives on the zero-cost typed wrapper as a [Conditional("DEBUG")] assert. The old {index32, gen24, kind8} form is superseded. |
| COM dispatch | dotnet10-csharp14-zero-alloc.md §4 (deep design: subsystems/com-interop.md) | Tiered. Hand-vtable calli on the per-frame hot path + any CCW invoked inside the frame loop; [GeneratedComInterface]/[GeneratedComClass] (source-gen, AOT-recommended) for all cold/warm COM (UIA, TSF, OLE, DWrite setup), isolated in FluentGpu.Windows (the Uia/ folder; or a thin satellite without [DisableRuntimeMarshalling] if that attribute conflicts with the source-gen in/out marshalling). ComWrappers is rejected on the hot path only (cache-lookup + call-site control — not a per-call alloc). The blanket "no ComWrappers anywhere / hand-vtable both directions" rule is superseded. |
| Threading / frame phases | hardened-v1-plan.md §2 + subsystems/threading-render-seam.md §14 | Render-thread seam. A PUBLISH(13a) phase splits the loop; record/batch/submit/present (8–11) run on a dedicated render thread reading an immutable SceneFrame; the UI thread owns no ComPtr/GPU object. The single-thread 13-phase loop in architecture-spec.md §4.8 / foundations.md §6 / README.md is build-order step 1, not the shipping topology. |
| Quarantine constant | hardened-v1-plan.md §2.3 + subsystems/threading-render-seam.md | QUARANTINE = RenderInFlightDepth (compile-asserted, consume-gated: reclaim a freed slot only when _lastConsumedSeq > freedSeq), with +1 slack. 0 in single-thread step 1. The literal 2 is superseded. |
| DrawList arenas | hardened-v1-plan.md §4.1 / §7 | Render-thread-private, ≥3-deep. Supersedes the 2-deep, UI-swapped design. The UI thread never swaps or resets a DrawList arena. |
| DepKey (hook deps) | subsystems/reconciler-hooks.md (+ README.md §5) | Pure-scalar blittable 16-byte struct. As built (2026-07, G1a — clean break): the params object[] deps overloads + DepsEqual(object[]) are DELETED (no [Obsolete] shim); DepKey is the sole deps shape, with implicit conversions (int/long/float/double/bool/string via a local XxHash64 / NodeHandle + a finite tuple set) + DepKey.Combine(a,b) + DepKey.FromRef(object?) = identity-hash + tag bit, SHIPPING (probabilistic). UseEffect with no DepKey = auto-tracked (re-runs on any signal read; DepKey is the explicit over-scoping opt-in). The exact reference-compare side GcDepTable (object?[] reset per render, ReferenceEquals) is the gated upgrade behind [EnableHookDepsLowering] (GEN-02), applied only if a FromRef collision is ever observed. A [StructLayout(Explicit)] [FieldOffset] union overlapping a GC ref with a scalar is illegal CLR layout (TypeLoadException) — see the archived archive/dsl-aot-toolchain.md trap. |
| Arena allocator | hardened-v1-plan.md §4.4 / §7 (supersedes foundations.md §6 single-buffer arena) | ChunkedArena + IVirtualMemory PAL seam (reserve-then-commit / segmented, addresses stable, no LOH/Gen2 copy). A native high-water counter — not the GC tripwire — gates chunk growth. |
| Validation / safety floor | subsystems/validation.md (+ hardened-v1-plan.md §4.5/§8) | Every ThreadGuard/alloc-tripwire/ComTracker/CleanSpanWitness/IsSpikeCaller assert is [Conditional]-erased from the shipping NativeAOT binary. In production, safety == CI coverage; a hazard not covered by a green gate or a retired spike is unguarded at runtime. |
| Clean-span validity | architecture-spec.md §5.4 + hardened-v1-plan.md §4.4/§4.6 | A memcpy'd clean span is valid iff every referenced handle IsLive and content-epoch unchanged and baked geometry matches. The witness captures a baked-geometry hash + (handle, gen, epoch); epoch validation is render-thread-local. |
| Translated (rebased) span copies | subsystems/scene-memory.md §4.3b (mechanism + soundness; subsystems/gpu-renderer.md §7 owns the InMotion payload fields, §11.1 points here; gates in subsystems/validation.md §3.6b) | A span whose subtree only MOVED is copied and patched per payload (not re-recorded): transform-carrying primitives get Transform.Dx/Dy; glyph runs get that plus InMotion = 1; clip/layer commands get their DEVICE rects offset (ClipCmd.DeviceRect/RoundedRect, PushLayerCmd.DeviceRect/CompositeClip, PopLayerCmd.DeviceRect), with InMotion = 1 on a Blur layer and OwnDmg*/DamageEpoch deliberately left STALE (mismatched epoch ⇒ whole-frame damage fallback). There is no opcode pre-check — the per-payload walk is the authority and its default arm fails safe. LayerKind.Acrylic vetoes the whole span (position-dependent backdrop) and the partial copy is rolled back. Eligibility requires ClipComplete at BOTH ends — which is what makes an offset clip rect exact — and a non-zero delta (a zero-delta copy would reuse bytes recorded under a different inMotion, defeating the settle re-snap). |
| Memo skip | subsystems/reconciler-hooks.md | 3-signal gate: SelfTriggered || propsChanged || HasConsumedContextChanged(slot). SubtreeDirty is the traversal scope only, never a skip-decision input. |
Tessellation (AS-BUILT 2026-08 — PathSweep/PathStroker/PathTessellator/PathRealizationCache, src/FluentGpu.Engine/Render/) | subsystems/gpu-renderer.md §5/§5.1 + hardened-v1-plan.md §4.3 | One vetted monotone/trapezoidal O(n log n) sweep (ear-clip deleted) with LOCAL crossing refinement (depth-capped recursive band bisection) in place of a global Bentley–Ottmann event queue. Complexity bound is SAFE-by-construction (self-intersecting fills correct by construction too); geometric correctness is fuzz-gated + cross-checked against IconRaster, an independent scanline rasterizer (PathSuite) — the originally-designed D2D golden-fallback (IPrimitiveFallback) did NOT ship, only the seam. PathTessellator takes destination Span<T>s, not ArenaAllocators (canon's signature forbids a retained realization; see gpu-renderer.md §5 for why). Trim/dash are deliberately NOT in PathRealizationKey (gpu-renderer.md §5.1). GpuProfile.PathAaMode supersedes canon's RenderConfig.PathAaMode (no RenderConfig type exists). |
PathRealizationKey (AS-BUILT 2026-08) | subsystems/gpu-renderer.md §5.1 (src/FluentGpu.Engine/Render/PathRealizationCache.cs) | record struct PathRealizationKey(int GeometryId, ulong ContentEpoch, ushort DeviceScaleQ, ushort StrokeWidthQ, byte RuleByte, byte JoinCapByte, byte Kind) (Kind: 0 = fill, 1 = stroke). JoinCapByte is additive beyond canon's printed 4-field key — without it two stroke nodes sharing one geometry/width but different joins collide on one cache slot. StrokeStyle.DashOn/DashOff and any animated trim are DELIBERATELY NOT in this key — they are per-frame PathInstance PS uniforms read against the baked PathVertex.S arc-length attribute, so a 60 Hz stroke-trim/dash animation stays a cache HIT with zero re-tessellation; folding them into the key would re-tessellate an animated stroke ~60×/s, defeating the cache's reason to exist. |
PathData / PathContentEpoch (AS-BUILT 2026-08) | subsystems/gpu-renderer.md §5 (src/FluentGpu.Engine/Foundation/PathGeometry.cs) | PathData is a sealed CLASS (not a record — a compiler with would let a caller clone-and-tweak around a stale epoch) with one constructor, epoch positional-first with no default; PathContentEpoch has no public constructor other than Mint(). Every construction site must therefore either write PathContentEpoch.Mint() inline or thread through one already minted for this exact content — the compile-time guarantee is narrowly "cannot construct a PathData without naming a freshly-minted epoch," not "cannot forget to bump content" in general (a caller can still misuse Mint() by reusing one epoch across genuinely different content). |
GpuProfile.PathAaMode (AS-BUILT 2026-08 — supersedes) | subsystems/gpu-renderer.md §5 (src/FluentGpu.Engine/Foundation/GpuProfile.cs) | canon printed RenderConfig.PathAaMode; there is no RenderConfig type anywhere in this repo. The as-built flag is GpuProfile.PathAaMode { Fringe = 0, Msaa4 = 1 }, default Fringe; Msaa4 is selectable but has NO backend (falls back to Fringe, counted via GpuProfile.NotePathMsaaFallback() rather than silently ignored). |
FillRule in hit-test (AS-BUILT 2026-08) | subsystems/gpu-renderer.md §5.1 (owns the rule) / input-a11y.md (consumes it) | Hit-testing a path shares the SAME PathData.Rule (nonzero winding default) the tessellator fills with — not just the baked vertices — so a click inside a complex path's hole behaves consistently with what is painted. |
| ThemedIcon mask primitive | opcode ENUM entry + VisualKind.IconLayer: subsystems/scene-memory.md §2.4/§4.1 · payload DrawIconMaskCmd + raster posture: subsystems/gpu-renderer.md §3 · control/registry/tokens: subsystems/controls.md + subsystems/theming.md | Layered vector icons ride the DirectWrite R8 glyph atlas + glyph PSO as CPU-rasterized colorless coverage masks, tinted per-instance via DrawIconMask. Geometry is interned in IconGeometryTable.Shared (a .Shared render-seam side-table keyed by int PathId, SpanRunTable precedent); backend rasterizes lazily on a (PathId, device-px) atlas miss. Explicitly NOT the §5 tessellation lane — a non-tessellation sibling like DrawTabShape, so the tessellation-fraction tripwire stays honest. Colorless mask + tint-on-command ⇒ retheme recolors with NO re-raster. |
| Color / coordinate / DPI | foundations.md P8 + architecture-spec.md §1.3 bet 4 | Brush color = straight-alpha sRGB float4 → renderer converts to linear-premultiplied at shader input. Swapchain BGRA8_UNORM buffer + RTV BGRA8_UNORM_SRGB (blend/resolve linear, hardware sRGB on write), output premultiplied. Text gamma is a deliberate exception. DPI applied once at layout→world; the per-window scale in that conversion is the EFFECTIVE scale = OS DPI (dpi/96) × app zoom (owner: subsystems/pal-rhi.md §1.2 — the app-zoom row below). Bounds is node-LOCAL. |
| Lane bitmask + phase-3 update queue | subsystems/reconciler-hooks.md §7 (storage: scene-memory.md UpdateQueueSlab) | Lane = 8-bit bitmask (Lanes helper; Lane.SyncInput = urgent, transition/await-continuation = non-urgent). Phase 3 is a real update queue, not a no-op: each setState enqueues an UpdateRecord{fiber, updater, lane} (MPSC ring); phase-3 HookFlush drains with lane selection + functional-updater fold ⇒ automatic batching across handlers and across await. RenderPriorityPolicy is the lane executor (which lanes flush this frame, anti-starvation watermark), not the priority source. GC-ref updaters via GcDepTable, never inline in the blittable slab. |
| Suspense boundary element | subsystems/reconciler-hooks.md (semantics) + scene-memory.md (storage) | SuspenseElement + SuspenseReveal enum + SuspenseSlot/SuspenseState + throw-free UseResource.MarkPending/SuspenseContext. Reconciler mounts the boundary fallback as a unit on pending descendant, atomically swaps to content on ready; nested progressive reveal + transition-aware keep-stale (keep revealed content during a P1 transition lane rather than flashing fallback, on the existing DetachedAnim slab). New VisualKind.SuspenseAnchor (Passthrough node) + SuspenseAnchor NodeFlags bit (storage owned by scene-memory.md). |
| External-store snapshot/version contract | subsystems/threading-render-seam.md §12bis (consumed by reconciler-hooks.md data-hooks) | IExternalStore<TSnapshot> = (uint Version, TSnapshot GetSnapshot(), IDisposable Subscribe(StoreChangedCallback)) (generalizes the proven ISystemColors Epoch/by-value-snapshot shape). Frame-start StoreReadLedger captures snapshot+version; pre-PUBLISH AnyVersionMoved tear re-check demotes to a blocking single-pass frame on mismatch. UseObservable/UseResource read through this seam. Adds no new lock-free surface (one volatile version word per store). |
| SelectionState column | scene-memory.md (column storage) + text.md (semantics) + input-a11y.md (drag/selection wiring) | POD SelectionState{Anchor/AnchorCp, Extent/ExtentCp, Affinity, Flags/Granularity} (~24B). Not a NodePaint field (preserves the 64B one-cache-line invariant) — stored via a sparse Dictionary<NodeHandle,Handle> index → SlabAllocator<SelectionState> with its own ContentEpoch + BakedRectsHash; written through the Mutate(SelectionHandle,…) chokepoint; selection is a new producer of the existing (Node,gen,epoch,bakedHash) clean-span witness signals. Read-side GetSelectionRects (BiDi visual fragments) backs both on-screen highlight and ITextRangeProvider. |
| FlowDirection column | scene-memory.md (column storage) + layout.md (resolution semantics) | FlowDirection enum (Inherit/LTR/RTL) + FlowState 4B hot-spine column {Inherited, Resolved}. Inherited as Context<FlowDirection>; resolved logical→physical at the WriteLayout boundary (phase 5) so the ported Yoga CalculateLayoutImpl stays physical and bit-for-bit with the golden-parity gate. LayoutPacked.ResolvedFlowIsRtl (1 bit, zero added bytes — LayoutInput stays 96B). |
| A11y collection-relation columns | scene-memory.md (column storage) + input-a11y.md (UIA semantics) | A11yInfo gains PositionInSet/SizeOfSet/Level/DescribedBy/FullDescription/FlowsTo/HeadingLevel/LandmarkType (via a cold A11yRel extension slab; A11yInfo 24→28B cold, A11yRelRef:int). Virtualizer feeds index+count; virtualized-provider realization contract — UIA Navigate can cause realization (scroll-to-realize). |
| DrawSelectionRectCmd / DrawFocusRing opcodes | gpu-renderer.md (struct shape + raster) + scene-memory.md (enum registration) | DrawSelectionRectCmd (per-BiDi-visual-fragment text-selection highlight; Rect+Radii+SelectionBrush+Affinity+Clip+Flags; behind-text z; solid premul-linear quad, not the text-gamma path; lowers onto shape_fill, zero new PSO). DrawFocusRing (the real Fluent focus ring on shape_border, one Params0-bit dashed/dotted reveal variant; the rectangular DrawFocusRect retained as the debug placeholder). Plus DrawScrimCmd (overlay dismiss-layer: modal-dim / transparent light-dismiss / blur-promote). All RenderLane.AnalyticSdf, overlay z-layer; SortKey reuses existing PassClass/RecordSeq (no new bits). DrawOp enum entries registered in scene-memory.md. |
| Gesture-arena tentative-capture | subsystems/input-a11y.md §7A | GestureArena/ArenaMember/ArenaVote{Pending,Accept,Reject,EagerAccept}/ArenaTeam. Pointer capture is tentative until arena resolution; e.Handled becomes an Accept vote into resolution, not the resolution itself; resolution can defer across pointer-move frames ("first to accept or last to not reject wins," eager-win, pointer-up sweep, hold/release). PointerFsm stays the per-recognizer implementation; the arena is the coordinator above it. |
| Pal.SetCursor seam | subsystems/pal-rhi.md (seam) + input-a11y.md (consumer) | IPlatformWindow.SetCursor(CursorId) + RegisterCustomCursor(...); CursorResolver arbitrates I-beam/resize/hand/busy along the L2 hit route (InteractionInfo.CursorId column is the source). |
Context-request routing + ClickRequestsContext | subsystems/input-a11y.md §6.5.1 | OnContextRequested = ONE declaration, FOUR triggers: ContextRequestTrigger ∈ {Pointer, Keyboard, Hold, Invoke} — Invoke = an activation (left-click / touch tap / Space-Enter) of a BoxEl.ClickRequestsContext node re-entering the funnel at that node (RequestContextFrom; Space/Enter dispatches Keyboard so a keyboard invocation still focuses the first item). ContextRequestEventArgs.Source = the originating node (Source == Node for Pointer/Keyboard/Hold); rect-anchored opens anchor on Source. Storage: the prop implies ClickBit (null click-handler column) + discriminator InteractionInfo.ClickRequestsContextBit = 1u<<16 (NOT in AnyInteractiveMask/self-hit); bit 16 ⇒ HandlerMask is uint (was ushort ) and every clear-site masks ~(uint)Bit. Mutually exclusive with OnClick. Supersedes the app-side RedispatchContextAt+OnRealized-capture pattern (the scrim keeps RedispatchContextAt). Gates: gate.ctx.invoke-* + E2.h. |
| One release, one owner (pointer gesture ownership) | subsystems/input-a11y.md §6.5 | A pointer release resolves exactly ONE gesture owner — nearest enabled self-or-ancestor with PressedBit or ClickBit — and delivers OnPointerReleased only if that owner took the press/release half, so a click-owning child terminates the walk with no release. The release walk formerly tested PressedBit alone, classifying an OnClick-only child with the genuinely inert ones (DragBit/CursorBit/SelectableTextBit) so click and release resolved to two different owners for one gesture with no shared handled state (a nested chevron toggled its drawer and raised its row's double-tap-to-invoke). The SAME owner keys the double/triple-click chain: two presses inside DoubleClickMs + the 4 px slop promote only on the same owner (owner-keyed, not hit-node-keyed, so a plate with inert children stays double-clickable). Gates: E2.n, E2.o. |
| Hover/press descendant cascade | subsystems/backdrop-effects-animation.md §7 | Cascade the REVEAL, never the control's own state. A container's hover/press edge drives a descendant's HoverOpacity/PressedOpacity across an interaction boundary (the reveal IS the container's affordance appearing), but drives HoverScale/PressScale only when the descendant is not itself interactive — the literal reading of this doc's "nearest interactive ancestor", since a nested button is its own. Boundary = HandlerMask & (ClickBit|PointerBit|PressedBit); it gates the scale leg and the recursion, never the reveal leg. Hover and press share one predicate (press formerly used a looser bare interact-row gate), the reconciler's lazy-affordance mount seed applies it, and SceneRecorder.nodeInteractive uses the same mask. Fill-only controls never cascade (no InteractionAnim row). A node with reveal AND scale is driven — opting into a container-driven reveal is the declaration. Gates: 58b, 58c, 58d. |
InputEvent POD + pump primitives | subsystems/input-a11y.md §3 (schema) + FluentGpu.Windows (Pal/) / FluentGpu.Engine (Headless/Pal/) (fill) | InputEvent is a blittable readonly record struct in namespace FluentGpu.Pal — (InputKind, Point2 PositionPx [DIP], int Button, int KeyCode, float ScrollDelta, KeyModifiers, PointerKind, bool IsRepeat, uint TimestampMs, uint PointerId, float Pressure) — PointerId/Pressure trailing-optional so mouse call sites (id 0, pressure 1) compile unchanged. No [StructLayout] (layout is not load-bearing — never reinterpret-cast/memcpy'd as a fixed blob; lives by value in the ring slab). The earlier 40B [StructLayout(Sequential)] size / TimestampUs / KeyOrChar / Vec2 PosDip forms are waived as not load-bearing (ms clock suffices; KeyCode/Point2 PositionPx are the as-built spellings). InputEventRing is a fixed-capacity drained-to-empty slab (Clear→fill→Drain one contiguous span), NOT a circular buffer (preserves the single-span Drain/Dispatch contract); per-PointerId move-coalescing + summed wheel, no Array.Resize. Ratified Win32 pump = EnableMouseInPointer(TRUE) (uniform WM_POINTER* for mouse/touch/pen; legacy WM_MOUSE*/SetCapture retired atomically) + GetPointerFrameInfoHistory (OS-coalesced sample drain), DIP-converted once. |
| Engine-owned scroll + WinUI wheel distance | subsystems/input-a11y.md §7B (integrator + tuning + velocity) + FluentGpu.Windows Pal/ (DirectManipulation phase producer + hi-res WM_POINTERWHEEL fallback) | The viewport scroll distance per wheel notch is max(48 DIP, 15%·viewport) (WinUI content-relative mouse-wheel line height), not a flat 60. Carried by InputEvent.WheelNotch/WheelNotchX device-notch fields (signed rawAmount/120, viewport-independent; the dispatcher scales them — a DIP-only ScrollDelta bypasses the scale, keeping the headless gates byte-identical; the flat element-handler DIP stays for WheelEventArgs.Delta). Scroll has ONE engine-owned offset mechanism: ScrollIntegrator plus SetScrollOffset/WriteScrollOffset are the sole clamp/scene/virtualization mutation path. The old OS-owned IScrollSource/ScrollSourceMux/Win32DmScrollSource design remains deleted. Windows may use manual-update DirectManipulation only as an event producer for touchpad phase intent (Scroll*/Momentum*); it never writes scene state, is absolute-deadline paced on the STA, falls back to hi-res WM_POINTERWHEEL, and yields synchronously to a positively identified physical mouse. The touch velocity sampler is a fixed-ring windowed least-squares regression (no short-flick under-read or stationary up-sample bias). |
| Resource budgets | budgets.md (new, consolidated) | Per-subsystem native/GPU/bandwidth budgets, eviction policies, failure behavior, and the open budget gaps. |
| macOS / cross-platform debt | macos-debt-ledger.md (new, consolidated) | Every Windows-specific decision, its macOS plan (if any), and status (Designed / Deferred / Unaddressed). |
| Project / library structure | architecture-spec.md §3 (mirrored in foundations.md §7, subsystems/README.md §2.6) | 4 libraries + 4 satellites = 8 projects, in src/FluentGpu.slnx under 5 solution folders (/UI-Rendering/ /Controls-Windowing/ /Windows-APIs/ /Tooling/ /Apps/). Libraries: FluentGpu.Engine (the portable engine core — RootNamespace=FluentGpu; the former Foundation/Rhi/Pal/Text/Scene/Render/Layout/Dsl/Hooks/Reconciler/Animation/Input/Media/Hosting + headless backends are now FOLDERS, namespaces verbatim), FluentGpu.Controls (portable control kit; refs Engine only; TerraFX-free), FluentGpu.Windows (swappable Windows backend; refs Engine + the one TerraFX.Interop.Windows package; folders Interop/ Pal/ D3D12/ DirectWrite/ Wic/ Uia/), FluentGpu.WindowsApi (OS-services scaffold). Satellites: FluentGpu.SourceGen + FluentGpu.Interop.SourceGen (netstandard2.0 Roslyn analyzers), FluentGpu.VerticalSlice (AOT harness exe), FluentGpu.WindowsApp (gallery exe / composition root). Engine never references Windows (compiler-enforced load-bearing direction); intra-Engine acyclicity (Dsl⊄Scene, etc.) is folder/namespace discipline (review-enforced). The vestigial FluentGpu.Rhi.Gdi was deleted. The earlier 27-project / "18-assembly" layout is superseded . |
| Control kit + devtools assemblies | subsystems/controls.md (FluentGpu.Controls) + subsystems/devtools.md (FluentGpu.Devtools) | FluentGpu.Controls = the SDK controls layer. As-shipped (Phase 0): a composition-factory hoist of the existing controls + per-control Style records — Button (+ nested Button.Style), IconButton, ToggleButton, Slider, ScrollBar, NavigationView (+NavItem/PaneMode), Navigator/Route/PageHost/Nav, Repeater (+RepeatLayout/RepeatKind), the Virtual factory, and Icons; VirtualListEl STAYS in Reconciler. Stated future target: the lookless ControlTemplate/ControlTheme/VisualState/ControlShell kit + the full 19-control set. Deps (ratified): Foundation, Dsl, Hooks, Animation, Scene, Reconciler (not the earlier "Dsl/Hooks/Foundation only"); stays acyclic — VirtualListEl is declared in Reconciler/, so the Controls → Reconciler edge is one-way with no back-edge. Adds no new column/PAL-seam/hook; the one new opcode DrawGradientStroke is owned by the opcode docs (see that row), not minted here. The two composition hooks UseHover/UsePressed stay ratification-flagged. As shipped (2026-07, G5 — the flagship control-kit rework, controls.md §4.6/§5/§6.5/§6.6/§7.0/§13): the universal controlled-input contract (concrete Signal<T> in + closed onChange/onClick/onCommit/onCancel/onOpenChanged set + auto-materialize); one X.Create idiom (Build banned public; per-control options records SliderOptions/TextBoxOptions/ListOptions; Slider is ONE Create, Bind/Ranged deleted); Button orthogonal axes ButtonAppearance{Standard,Accent,Subtle,Outline} × shared ControlSize{Small,Medium,Large} + Button.StyleHook (AccentStyleOverride/StandardStyleOverride deleted); InteractionRecipe + presets + BoxEl.Interactive; controlled Popup + Toast/ToastHost (auto-mounted lane) + MenuSafeTriangle over the overlay manager; the registry router (RouteDef/RouteRegistry/[Route]/PageHost.Create(nav, routes)); the kit localization keys (Loc.Bind→Prop<string>, ~41 neutral keys, PseudoLocalizer, FGRP008). Analyzers FGRP003-008 + the [Props]-generator diagnostics FGSG001-005 land alongside. FluentGpu.Devtools = dev-only live inspector/profiler (read-mostly observer; IDevtoolsObserver+DevtoolsBus; behind FluentGpu.EnableDevtools/[Conditional("FG_DEVTOOLS")] ⇒ 0 bytes in release; when attached, QUARANTINE = RenderInFlightDepth + (devtools attached ? 1 : 0) + 1). |
Reactive element-prop surface (Prop<T>) | subsystems/reconciler-hooks.md §0bis (semantics) + subsystems/dsl-aot.md (record shape) | ONE Prop<T> property per bindable channel — BoxEl Transform/Opacity/Fill/Width/Height, TextEl Text/Color, ImageEl Source/Placeholder — accepting a static T (re-asserted each reconcile iff not bound), a Func<T> thunk, or a concrete signal (signal-direct, no closure; Prop.Of(...) wraps inline lambdas — C# cannot chain a lambda conversion into a user conversion). Bind wiring is mount-only (a fresh thunk on re-render is ignored — change the signal's value, not the bind). The decomposed OffsetX/Y/ScaleX/Y/Rotation floats stay static sugar composed only when Transform is unbound (the identity value-gate is load-bearing for ItemsView displacement/sticky/FLIP). The dual static+*Bind surface is superseded . As built (2026-07, G1a): the implicit-conversion set is T / Signal<T> / Memo<T> / FloatSignal / Func<T> (retained — deleting it churned hundreds of lines for zero gain); Prop.Of(...) wraps inline lambdas; Prop.Bind<T>(IReadSignal<T>) is the named ctor for the interface case (C# can't chain a lambda→user conversion). default(Prop<T>) = static default(T) (the HasValue bit was REJECTED — it grows {T}Diff); the FGRP007 analyzer flags a Prop<T> element channel declared without an initializer. |
| Component activation lifecycle | subsystems/reconciler-hooks.md §0bis (hook semantics + the Activation.IsActive ambient) + subsystems/pal-rhi.md (window-visibility source via IPlatformWindow.State) | A notify-only lifecycle: UseIsActive() → IReadSignal<bool> (reactive truth) + UseActivation(onActivated, onDeactivated) (transition callbacks, edge-only — never at mount/unmount). Inactive = parked by Flow.KeepAlive (per-component) OR window minimized / app-suspended (app-wide). Per-component half: a lazily-created Signal<bool> on the reconciler's CompEntry, flipped in the existing SetSubtreeParked chokepoint. App-wide half: a host-owned Signal<bool> published as the ambient Activation.IsActive, written on the minimize/restore edge in RunFrame (a one-shot flush on the minimize edge fires onDeactivated before the gate's early-return); the app may AND-in power suspend via the public AppHost.SetWindowActive(bool) (engine never references the power API). UseActivation is backed by a STANDALONE effect (not the render-effect, which is suspended while parked). Engine auto-quiesce (same SetSubtreeParked edge): a NodeFlags.Parked marker + per-node AnimEngine.SetNodeParked/ScrollAnimator.SetNodeParked make the tickers skip parked tracks and exclude them from HasActive (O(1) counter), so a backgrounded tab's looping animation / mid-fling scroll cannot defeat the idle wake-stop. Notify-only: the engine signals; the developer pauses their own work. Focus/blur and same-screen occlusion are not inputs. Entirely UI-thread; no render-seam interaction. As-built: RenderContext.cs (UseIsActive/UseActivation), Reconciler.cs (CompEntry.ActiveSig/SetSubtreeParked/OnNodeParkedChanged), AppHost.cs (_windowVisible/SetWindowActive), Animation\{AnimEngine,ScrollAnimator}.cs (SetNodeParked), NodeFlags.Parked. As built (2026-07, G4b — component-model unification): there is ONE Component base — ReactiveComponent/RunsOnce/InvalidateTree are DELETED; every Render() is tracked and run-once is inferred (a render that reads no signals never re-runs). Each component's render-effect + hook cleanups are owned by one per-instance ReactiveScope (CompEntry.Scope, disposed at unmount); hook cells are call-site-keyed (HookKey{FileHash, Line, Ordinal}) so conditional/looped hooks are legal (FGRP005 → compatibility lint). Owner: reconciler-hooks.md §0bis AS-BUILT (2026-07). |
| Virtualization seam (E11 L0–L3) | subsystems/virtualization.md (seam contract) + subsystems/controls.md (L3 composition) | IVirtualLayout (pure, alloc-free (count, viewport, offset) → window + rects) + IMeasuredVirtualLayout : IVirtualLayout (SetMeasured/OffsetOf/IndexAt — estimate-then-correct + scroll anchoring behind the SAME seam, user-implementable) + IViewportVirtualLayout : IVirtualLayout (SetViewport(mainExtent, crossSize) — the engine feeds the scroll-axis viewport before geometry so a layout can size items to it, e.g. fill-the-width shelves; user-implementable) + ISplicingVirtualLayout : IMeasuredVirtualLayout (ItemCount + Splice(at, removed, inserted) — the STRUCTURAL count change: a composing control that knows the band it inserts/removes hands it over before layout observes the new count, so every surviving row keeps its corrected extent; a bare count change RESIZES rather than re-seeds, ExtentTable.Resize/Splice; user-implementable). One substrate for every items control: built-ins Stack/Grid/HorizontalGrid/FillRow/LinedFlow/SpanningGrid/MeasuredStack/GroupedList (group header = a measured item kind; sticky hook StickyHeaderIndexAt); L1 VirtualListEl consumes both seam kinds + realize-edge lifecycle (OnItemPrepared/OnItemClearing/OnItemIndexChanged/OnVisibleRange/OnRealized); L2 Repeater.ItemsRepeater (typed templates, ItemCollectionTransition → FLIP); L3 SelectionModel (sorted disjoint index RANGES — select-all realizes nothing; WinUI Single/Multiple/Extended selector semantics) + ItemContainer + ItemsView. As-built: FluentGpu.Engine\Scene\VirtualLayout.cs, FluentGpu.Engine\Reconciler\VirtualListEl.cs, FluentGpu.Controls\{Virtual,Repeater,SelectionModel,ItemContainer,ItemsView}.cs. As built (2026-07, G5h): ItemsView.Create(itemCount, template, RepeatLayout, ListOptions?) is the canonical host; the ~20-arg tails fold into the ListOptions/ListOptions<T> record (Selection/Invoke/Overscan/Selector/KeyOf/Transition/CountSignal + grouped Scroll/Reorder/Entrance sub-records) + the knobs ContentType/CacheExtentPx/RepaintBoundary + keep-alive slots (KeepAlive predicate + KeepAliveCap LRU, off-window rows keep live state). VirtualListEl is unchanged and stays in Reconciler; the Virtual.* factories stay PUBLIC (advanced L2 substrate; RepeatLayout presets are the recommended surface — G7-adjudicated, no demotion). Zero-alloc recycling untouched. |
| Controls scroll-controller seam | subsystems/controls.md §13 | IScrollController is the portable two-way push/request contract: a viewport calls SetValues(min,max,offset,viewportLength) + SetIsScrollable; a controller raises typed ScrollToRequested/ScrollByRequested. ScrollOptions.VerticalScrollController wires ItemsView; ScrollControllerAdapter.Attach wires a plain vertical ScrollEl; both serve requests through the existing single ScrollIntoView writer and compose, rather than replace, the caller's change-gated geometry observer. ItemsViewController.TryGetItemIndex(hRatio,vRatio,out index) is the live layout/window oracle used by detail affordances. This is a Controls contract only and mints no engine primitive/column/opcode. AnnotatedScrollBarController is the stock signal-backed implementation. |
| SemanticZoom retained-view control | subsystems/controls.md §13 (control/API) + subsystems/backdrop-effects-animation.md §4 (motion recipes only) | SemanticZoom.Create(SemanticZoomSlots, SemanticZoomOptions) retains exactly two named views through the existing Flow.KeepAlive(MaxEntries:2) substrate. The control owns controlled zoom state, SemanticZoomController.ToggleActiveView/ZoomOutTo/ZoomInTo, bidirectional item-index maps, started/completed notifications, focus/Escape behavior, and item anchoring through ItemsViewController before presentation. Each view keeps its own scroll identity. Directional motion is MotionRecipes.SemanticZoomOut/In: opacity plus scale (overview 1.08→1 while detail recedes 1→0.94, reversed on zoom-in), never root blur; reduced motion is resolved as a token value by the animation system, never an app branch. No engine primitive is minted. |
Form validation (Validator/Field/Form) | subsystems/form-validation.md (+ control wiring in controls.md §5.7) | Validity is a derived value, never stored error state. Rules = Validator<T> delegates returning a loc-key MsgId (MsgId.None==valid); a field's displayed error = a gated Memo<FieldError>, submit gating = an ungated Memo<bool>. Cross-field/conditional rules are FREE (a rule reads a sibling signal inside the memo → auto-re-validates). UseField/UseForm (same-component fields join a thread-local form-under-construction; nested via Ctx.Provide(FormScope.Context,…)). ONE new bound channel Prop<ValidationState> BoxEl.Validation → the reconciler resolves Tok.SystemFillCritical on the UI thread into NodePaint.ValidationBorder (equality-gated; the recorder forces a SOLID error ring over any gradient border — render stays theme-agnostic). Messages are localization keys resolved at the bound TextEl thunk (i18n + culture-reactive, 0-alloc on a hit). Async = a debounced server check merged race-immune via an equality-gated signal (UsePost + per-field CTS). Default timing OnTouched. 0 managed alloc on the keystroke path; no DataAnnotations / reflection / expression-trees. Optional [Validatable] source-gen (FluentGpu.Validation.SourceGen) lowers to the SAME Validator<T>. A11y rides input-a11y A11yInfo+UseAnnounce (its column work is this subsystem's first consumer). Distinct from validation.md (CI spikes/gates). |
Animation engine (REWORKED — LANDED + verified; design docs/plans/animation-engine-rework-design.md, research …research-dossier.md) | subsystems/backdrop-effects-animation.md §5 (semantics, owner) + the rework plan | Canonical value (as-built): one POD AnimValue slab keyed (node, channel) {value, velocity, target, generator} (interpolates-from-current + auto-retargets on signal change) driven by the class still named AnimEngine (the slab scheduler — PASS1 advance / PASS2 fold-and-write-once compose / PASS3 free; one write per node×channel); the analytical closed-form spring (3 regimes: under/critically/over-damped) sampled at absolute t — replaced the sub-stepped Euler, dt-deterministic by construction; the declarative surface Element.{Transition,While*,Enter,Exit,Stagger,Layout} (the reconciler bakes to seeds via AnimBake/SynthesizeDeclarative) + one MotionTok registry (subsumes Dsl.Motion/Dsl.Expressive/MotionSprings) + reduced-motion as a value (AnimScheduler.Structural.ReducedSnap, never a hook branch); side-table channels {BrushFade, HoverFade, PressFade} subsume the deleted AdvanceBrushAnims ticker + the deleted InteractionAnimator (hover/press cascade moved into the engine); FLIP relativeTarget; ConnectedAnimation→DetachedAnimSlab+SceneRecorder.RecordDetached rebuild behind FG_DETACHED_FLY (default-off = the proven live-overlay path). As-built: FluentGpu.Engine\Animation\{AnimValue,Generators,AnimClock,AnimScheduler*.cs,AnimTypes,MotionTok,AnimOwner,DetachedAnimSlab,ConnectedAnimation}.cs + Hooks\DepKey.cs + Reconciler\AnimBake.cs. Verified: 521 VerticalSlice gates (zero-alloc phases 6–13 + dt-determinism). Residual PERF follow-ups (within the near-zero edge-alloc bound — steady frames are 0-alloc): index-based SignalSource (retires the DrivenClockTable closures), the shared multi-keyframe arena (per-call Keyframe[] at one-shot seeds), and the explicit per-track CadenceClass (the wake decision is already correct via AnimIsAmbient+AmbientAnimationFps). Consumer cross-ref (2026-07, G5a): the control-kit InteractionRecipe (controls.md §6.6) is a consumer of this engine — brushes ride the HoverFade/PressFade/BrushFade side-table channels, geometry rides the WhileHover/WhilePressed MotionTargets + a MotionTokenId; it mints no new animation primitive. |
| Self-blur pin cache / key | subsystems/backdrop-effects-animation.md §FA-2a (gpu-renderer.md §7 owns the PushLayerCmd.InMotion payload field) | Cross-frame retained self-blur pin (the OpacityLayerCompositor region-pin pool); computed by portable FluentGpu.Render.BlurPinKey.TryCompute. Key = σ + integer device size round(DeviceRect.W/H) + the subtree op bytes with every op's Transform.Dx/Dy (and ClipCmd rect origins) rebased to the layer origin + rounded to integer device px (translation-invariant; scale/rotation + content fold verbatim; self-validating on content — a nested/unknown op ⇒ uncacheable). MRU-on-hit LRU (a FindPin hit refreshes LastUseFence), PinBudget=24 region pins (≥ 8 reserved for canvas scratch), LayerTargetTrim.PinIdleFrames=120 submitted frames (the shared trim policy — see Layer-target pool sizing + trim + census below), one settle re-mint at rest (PushLayerCmd.InMotion==0 + moved). A position-only move is a HIT for every BlurCachePolicy; σ/size/content/≥1px-relative-move stay content misses (scale reuse is a non-goal — size is content). Distinct from the acrylic LayerId backdrop cache (backdrop-effects-animation.md §2.3). |
| Layer-target pool sizing + trim + census (NEW — LANDED 2026-09) | subsystems/gpu-renderer.md §7.1 (the LayerPool owner) | Canonical values, all in portable FluentGpu.Render and headless-gated as gate.layerpool.*. SIZE: two ladders on purpose — AcrylicBackdropMath.BucketDim stays next-power-of-two, floor 64 (the dual-Kawase pyramid halves each level), while the opacity/blur pool uses LayerTargetBucket.Dim = 64-px steps to LinearCeiling=2048, powers of two above (damage boxes cluster in the few-hundred-px range where po2 wastes up to 4x the area, and the po2 step straddling a window WIDTH — 1195→2048 — cancelled a full-width bounded band's whole saving); reuse survives the finer ladder because every lease is best-fit ≥ the bucket with the SampleWindow used-sub-rect clamp, and a bucketed pair coinciding with the canvas size is harmless because a slot is classified by SIZE, not provenance. BOUNDED TARGETS: a group RT is canvas-sized only when the backend cannot bound the subtree — the region-local self blur, the exact (down == 1) blur's ping-pong scratch (region + a full tap guard, cleared, H-pass scissored to the region ⇒ pixel-identical to the canvas scratch), and a recorder-patched plain Opacity group whose subtree is FLAT and STENCIL-FREE (LayerSubtreeProbe — stricter than BlurPinKey.TryCompute, which admits stencil ops) sized to BoundedGroupRegion.Compute over PushLayerCmd.CompositeClip; each is entered under a viewport shifted by -Origin and mapped by LayerTargetMap.For/Sweep against the SURFACE dims, never the used extent. TRIM: LayerTargetTrim.Classify on the fenced frame boundary — in-use never; pins PinIdleFrames=120 both tiers; scratch IdleFramesWeak=120/IdleFramesStrong=600; canvas-sized the same window except the WarmCanvasReserve=2 most-recently-used, all released past ColdIdleFrames=900 (which must stay strictly greater than both ordinary windows), plus an immediate weak-tier WeakCanvasHardCap=4 on idle canvas slots (adreno M5). "Retire" ≠ "release": retiring queues; releasing is gated on LayerTargetTrim.CanRelease(lastUseFence, completedFence), the threading-render-seam.md deferred-reclaim convention — so a trim can never free a surface a submit in flight references. CENSUS: LayerTargetCensus (in-use/free/pin/retired bytes + counts) per compositor, summed into the gpu census line as rt: inuse=… free=… pin=… retire=…, because gpu bytes is one total and cannot say whether the biggest class is working or merely resident (on UMA, resident = working set); BakedBlur's scratch banks are created lazily on the first bake for the same honesty. |
Unified media playback API (LANDED — M0–M5 green; full design docs/plans/media-playback-api-spec.md; DRM docs/plans/video-drm-layer-design.md) | subsystems/media-pipeline.md (subsystem owner — FluentGpu.Media present-tree §8 + the DRM attach §8.4; seam IVideoPresenter shape owned by pal-rhi.md) + the media spec (deep API) | One headless IMediaPlayer (state as IReadSignal<> signals, transport as idempotent coalescing verbs) fronted by the MediaPlayer facade + MediaRouter (routes by MediaSource.Kind, swaps the inner backend), fulfilled by two backends behind the per-platform video seam IMediaBackend/IMediaSession/VideoDelivery: PcmAudioPlayer (portable 5-stage pull graph) and Windows MfMediaPlayer (MF video + the native in-process PlayReady DRM path). Source model = the immutable MediaSource + algebra (From*/Concat/Clip/Loop/Merge/With*). Audio-graph seams: IAudioSink/IBufferedAudioSink/IAudioClockSource/IDeviceWatcher/IDspStage/AudioGraphHost (immutable graph PUBLISHED by atomic swap, freed under RenderInFlightDepth+1 consume-gated quarantine) + CrossfadeMixer (N-voice; gapless = crossfade with overlap 0). DRM = the managed WithDrm relay Func<LicenseRequest, ValueTask<LicenseResponse>> — native CDM raises the challenge, managed does the license POST, the CDM sees no key/decrypted pixel; the protected DComp handle attaches at the unchanged IVideoPresenter.BindSurfaceHandle. Decrypt-on-read = the DecryptingSource IMediaByteSource decorator over an app-supplied IAudioKeyProvider (PlayPlay). Playback-quality ownership (ICancellableAudioSource, transferable PCM rings, AudioTransitionGate, finite-capacity headless output) is owned by media spec section 7.9. Cross-backend preroll = IPreparableBackend (queue/router-level PreparedSlot generalization; seek-invalidated Epoch). PositionSeconds (FloatSignal) is a lossy one-way UI projection; the authoritative timeline is integer (WASAPI played-frames / MF 100-ns ticks) — every precision-critical op computes in the frame/tick domain. Cross-backend mixed audio↔DRM-video queue is a v1 requirement (A↔B is a declicked HARD CUT, never a cross-backend crossfade). As-built: src/FluentGpu.Engine/Media/Playback/ (+ Media/Playback/Audio/, Media/Playback/Adaptive/) and the image-decode subsystem src/FluentGpu.Engine/Media/Images/ (folder split landed G7, namespaces unchanged), src/FluentGpu.Windows/{Media,Wasapi}/, src/FluentGpu.WindowsApi/Media/PlayReady/. Verified by src/FluentGpu.Engine.Tests (91) + src/FluentGpu.Windows.Tests (59), NOT the VerticalSlice harness (user directive). |
| Re-pushed component props channel (NEW — LANDED, G4c–G4e) | subsystems/reconciler-hooks.md §8bis (+ authoring contract subsystems/component-props-contract.md) | A per-instance parent→child live-data channel, distinct from context (broadcast). Embed.Comp<T,TProps>(TProps props, Func<T> factory) + non-positional UseProps<T>() over a per-instance Signal<object?> CompEntry.PropsSig; delivered at the reconciler ComponentEl reuse seam as reference-short-circuit → record-equality gate → Runtime.Batch coalesce (parked children defer + replay latest). Sugar = the [Props]/[Prop] generator (PropsGenerator → per-field Signal<T> + IPropsHost.ApplyProps per-field-gated writes + Of(...)/CurrentProps()/From(...) + PropsManifest; delegate props ride a stable latest-write forwarder; diagnostics FGSG001-005). Superseded the ~19 hand-rolled Props.Channel/EnabledChannel/SlotsChannel control workarounds (G4d, deleted). As-built: Hooks/{ComponentEl,PropsAttributes}.cs, Hooks/RenderContext.cs (UseProps), Reconciler.cs (reuse-seam delivery), SourceGen/Engine/PropsGenerator.cs. |
HostTimerQueue (UI timing cadence) (NEW — LANDED, G1b) | subsystems/reconciler-hooks.md §4 (timing hooks consume it) — impl FluentGpu.Engine/Hosting/HostTimerQueue.cs, owned by AppHost | A min-heap of one-shot (dueMs, generation, callback) timers on the host frame clock (wall clock for a real window; deterministic frame-delta headless), owned by AppHost and drained after DrainUiPosts and before the reactive flush (so due writes coalesce into this frame's re-render). Backs UseDebouncedValue/UseThrottledValue/UseTimeout/UseInterval + RecommendedWaitMs/WakeFrame integration (idle-quiesce preserved: empty heap = one comparison, 0-alloc; UseInterval auto-pauses when parked/minimized). NOT System.Threading.Timer and NOT the media/AudioFeedThread clock (the media engine keeps its own device clock — WS4 non-goal). |
Video hole punch (DrawVideo = 17) (AS-BUILT 2026-07) | subsystems/gpu-renderer.md §3.1 (payload shape) + §7.3 (raster, ordering, limitation) — enum registration + VisualKind.Video in subsystems/scene-memory.md §2.4/§4.1; present/registry behavior in subsystems/media-pipeline.md §8.2/§8.3 | Canonical value: DrawVideoCmd(RectF Dst, CornerRadius4 Radii, int SurfaceId, float VideoReady, Affine2D Transform, float Opacity) — node-local Dst + baked world Transform, like FillRoundRectCmd; the earlier 7-field spec form (Surface/PosterBlur/AlbumArt/Clip) is superseded , reconciled field-by-field in docs/plans/video-compositing-spine-design.md §5.1. Replay ERASES the already-painted UI pixels in Dst — a DestOut blend (ZERO/INV_SRC_ALPHA, color+alpha) on a third RoundRectPipeline PSO riding the existing rounded-rect SDF shader, giving with coverage AA, per-corner radii and both clip tiers inherited; its own run class (PrimKind.VideoHole) keeps an A=1 hole out of the opaque no-blend PSO (which would paint it solid black). Ordering is painter/tree order only — no PassClass (the hole is emitted at the video node's paint slot; letterbox/chrome are later-painting siblings that repaint over it). SurfaceId = the VideoSurfaceRegistry slot token, diagnostic at replay (the presenter places its DComp child visual independently). Limitation (accepted, not a gap): the erase hits the bound RT, so inside an offscreen opacity/blur/acrylic layer it never reaches the back buffer — main canvas only; cumulative parent opacity likewise attenuates it. The recorder emits a constant VideoReady = 1; the graded art→poster→live crossfade is deferred (a poster-drawn-after pattern must grade the poster, not the erase). Under FLIP_DISCARD the hole re-punches every frame; damage inflation is deferred to partial present. |
Drop-spotlight scrim + EraseRoundRect = 18 (AS-BUILT 2026-08) | subsystems/gpu-renderer.md §7.4 (payload shape, DestOut raster, the recorder BAND-ORDER table, the scrim emission) — enum registration in subsystems/scene-memory.md §4.1; drag POLICY (DropTargetVisualPolicy, DropTargetSpec.SpotlightWhen, SceneStore.SpotlightScrimClip, the spotlight-root refresh edge) in subsystems/input-a11y.md §12 | Canonical value: EraseRoundRectCmd(RectF Rect, CornerRadius4 Radii, float Strength, Affine2D Transform, float Opacity) — a GENERAL rounded-rect DestOut erase reusing DrawVideo's SDF shader, RectInstance, DestOut PSO and PrimKind.VideoHole run class verbatim (no new shader/PSO/texture/RHI method), minus the surface identity. Band order (authoritative): main pass -> orphan fallback -> drop-spotlight scrim -> drag ghost -> connected-animation overlays -> drag overlay (chip); the scrim is the top of band 0 (depth (1<<16)-1), so hoisted drag visuals stay lit by construction. The scrim is ONE opacity group at DragVisualTok.ScrimOpacity (0.55) holding a DragVisualTok.ScrimColor veil plus one erase per compatible destination (destination rect INTERSECT its ClipsToBounds ancestors INTERSECT SpotlightScrimClip, with the destination's own corner radii). Supersedes the per-node opacity multiply/divide dim: DragVisualTok.SpotlightBackgroundOpacity (0.28) and the SetDropSpotlightExempt/IsUnderDropSpotlightExempt presentation-only registry are deleted . Limitation (accepted): the erase is layer-local (gpu-renderer.md §7.3), so the scrim band must not itself be nested in another offscreen layer; and the scrim colour is a single theme-blind constant (the recorder takes no theme colour beyond focus/scrollbar/text-edit), so a light-theme softening would need a new host-plumbed colour. Gates: e5dragdrop.scrim.{cutout,policy,clip,band,cancel,alloc}. |
| Drag & drop: the LIFT contract + the drag visual bands (AS-BUILT 2026-08) | subsystems/input-a11y.md §12 (the contract + all policy: DragLift, DragVisualStyle, DragSession/DragState, DropTargetSpec incl. SpotlightWhen/RefusalCaption/spring-load, dead-source tolerance, the settle window, the phase-7.8 spotlight refresh edge, the known limits) + subsystems/gpu-renderer.md §7.4 (the recorder band order) + subsystems/controls.md §7.4 (the Drag/Drop facade, DragChip, DragPreviewLayer, InsertionOptions/SortableMath) | Canonical value: DragVisualStyle.Lift : DragLift { Ghost = 0, Stationary = 1 } selects between TWO mutually exclusive drag visuals, and exactly one owns the source node's presented channels for the gesture. Ghost (default, byte-identical to the pre-chip engine) = the source row IS the visual: translate + Opacity + Shadow/Scale + one OpacityGroup over the subtree + optional opaque Backplate + NodeFlags.DragGhost (recorder ghost band), clamped to the root rect in RetargetFromRest. Stationary = the source row keeps its slot and receives ONLY the dim and ~HitTestVisible; the moving visual is a DragPreviewLayer chip in the SceneStore.DragOverlay band, following the pointer through the bound InputHooks.DragPosX/DragPosY signals (compositor-only) with InputHooks.DragEpoch EDGE-triggered (begin/end, target/effect/refusal/caption, settle). Scene scalars minted for it: SceneStore.DragOverlay, DragGhostBackplate, DragSourceOpacityOverride (the destination's override of the Stationary source dim — the ONE sanctioned second writer, cleared unconditionally in DragController.Reset — the single chokepoint every exit path funnels through, since RestoreVisuals is skipped whenever the source node is already dead), SpotlightScrimClip. A Stationary session SURVIVES its source being freed (DragController.SourceRecycled; DragDropContext.PruneDead reparents Source onto the scene root, the ExternalBegin shape) while a Ghost session aborts through OnAbandoned → DragDrop.Cancel(). AnimScheduler.Compose skips drag-owned nodes so hover/press motion cannot fight the drag visual. Gates: e5dragdrop.{touch,prune,reassert,animconflict,facade,springload,block}, e5dragdrop.chip.{stationary,compositor,band,clamp,survive}, e5dragdrop.ghost.{layer,clamp}, e11virt.{prefix-disp,insertion}, sortable.{slot,gap,empty,normalize}. |
VideoSurfaceRegistry pump/ownership seam (NEW — LANDED, G5g) | subsystems/media-pipeline.md §8.3 (seam IVideoPresenter/VideoSurfaceId shape owned by pal-rhi.md) | The single-writer video-pump seam that moved per-frame PumpVideo/SetViewport out of Render (the WS-MediaUI anti-pattern) onto engine phase 7.2. delegate void VideoPump(float scale); RegisterPump(token, owner, pump) / UnregisterPump(regId) / TransferOwnership(token, owner) (first-class fullscreen hand-off — enforces exactly one pumping owner, replacing the old convention + conditional hook) / IsPumpOwner / PumpPending(scale) (owner-only; suppressed non-owner calls counted). Driven by AppHost at phase 7.2 (_videoSurfaces.PumpPending(...), after RunAfterAnimations, before the phase-11.5 Drain). As-built: FluentGpu.Engine/Media/Playback/VideoSurfaceRegistry.cs + Hosting/AppHost.cs. A `DEBUG |
| Video-engine snapshot/command seam (NEW — video-smooth-switching rework) | subsystems/media-pipeline.md §8.3 (narrative) — types in src/FluentGpu.Engine/Media/Playback/VideoEngineSeam.cs | Single-writer VideoEngineSnapshot published through a seqlock (VideoSnapshotBuffer.Publish/Read — alloc-free; the engine MTA thread is the SOLE writer) + a LAST-WINS coalesced VideoEngineCommandQueue (VideoCommandKind per-kind slot Post/TryTake) replace the deleted blocking IVideoEngine.Invoke<T>/InvokeSlot<T>/InvokeSlotPool<T>/NativeSizeAnswer family and the 10 Hz poll timer — MfMediaSession.PumpVideo reads exactly one snapshot per pump, zero blocking calls and zero COM touches on the UI thread. Publish-then-raise StateChanged ordering (a woken listener reads at least the state that raised it). MfMediaPlayer warm-engine lease/return (LeaseEngine/ReturnEngine, a per-source SourceEpoch stale-state guard, sticky VideoEngineFlags.Faulted rebuild) replaces full engine teardown/rebuild per track change. Portable, engine-free, TerraFX-free (FluentGpu.Media); gated headlessly by the VerticalSlice MediaSeamSuite. Full plan: ../plans/video-smooth-switching-implementation.md §1. |
| Node transform ownership (AS-BUILT 2026-07) | subsystems/scene-memory.md §2.4 (the NodePaint.LocalTransform column) | Exactly ONE writer per node's LocalTransform. An element declares it in one of two static spellings — an explicit Transform matrix, or the decomposed OffsetX/OffsetY/ScaleX/ScaleY/Rotation floats — and the matrix wins; a bound Transform (thunk/signal) supersedes both. None of these may be combined with a transform-owning ScrollBind (PinTop / StretchFromTop / MorphLeftTo / MorphTopTo / a TransX|TransY|Scale* sink) or with transform-channel animation, which rewrite the matrix every frame. A [Conditional("DEBUG")] reconciler tripwire turns each combination into a stack trace at the offending element. A static matrix used to be silently DROPPED (read only when bound), so an authored offset compiled, ran and moved nothing. Gate: gate.reconciler.static-transform. |
| Overlay-stack (ZStack) alignment (AS-BUILT 2026-07) | subsystems/layout.md §3.7 | A ZStack has no main axis, so both axes are alignment (the WinUI overlay-Grid model): vertical from AlignSelf falling back to the stack's AlignItems, horizontal from JustifySelf falling back to the stack's Justify (a distribution read for its alignment sense; Space* ⇒ Start). An auto-sized child that is Center/End on an axis takes its desired extent there rather than stretching — otherwise it fills the slot and there is no free space left to align it within. Start/Stretch/Auto keep the content-origin, stretch-to-fill behaviour, so a stack that authors neither arranges exactly as before. Gate: gate.layout.zstack-align (+ check 55 as the no-regression pin). |
| Named scroll timelines (AS-BUILT 2026-08) | ../plans/generic-hookable-scroll-engine-design.md §5.1 (owner: the ScrollBind model, the DSL, the deferred-resolution rule, the two author rules) — the LocalTransform single-writer rule it composes under stays owned by subsystems/scene-memory.md §2.2 | Canonical value: the CSS pair, ported verbatim — ScrollEl.ScrollTimeline / VirtualListEl.ScrollTimeline / ScrollOptions.ScrollTimeline publish a scroller's progress under a NAME (scroll-timeline-name), and ScrollBindDsl.Timeline consumes one (animation-timeline: --name). Why it exists: a bind's driver is otherwise the nearest ANCESTOR Scrollable and a driverless bind is dropped, which cannot express a page-root backdrop (wash / blurred artwork / parallax plate) that must be a ZStack SIBLING of the scroller — clipped or painted over by it, or deliberately exposed through a sticky band's clip — yet has to move with its content; such a layer was permanently viewport-anchored. Resolution is DEFERRED to the end of the reconcile pass (ScrollBindTable.ResolveNamedTimelines, from RenderRootDiff + ReRealizeVirtuals): consumer and publisher can be baked in either order and the motivating case bakes the consumer FIRST (backdrop = ZStack child 0, page = child 2), so a named row is added with a null scroller, joins the node's teardown chain but no eval chain, and links when the name has a live publisher. An unresolved name is inert, not an error. Once linked it is an ordinary row that happens to hang off a scroller it is not inside — eval chain, sinks, change gate and the zero-alloc contract are untouched. Two author rules: (1) ONE live publisher per name — last registration wins and two pages are co-mounted mid-navigation, so scope the name to the content identity as ScrollKey is, never a bare constant, and never share one across nested scrollers (the inner sits at a permanent offset 0); an unmount retires the name it published. (2) CONTINUOUS ops only — PinTop / ClipTopAtViewport / StretchFromTop / MorphLeftTo / MorphTopTo / SignedPhase / the ScrollRange.Enter anchors all measure the target's position INSIDE the viewport, which has no answer for a non-descendant, so each throws in DEBUG; Frac/Overscroll anchors stay valid (scroller-derived). Gates: gate.scroll.named-timeline (+ its unnamed-control arm), gate.scroll.named-timeline-retire. |
| Programmatic bring-into-view (AS-BUILT 2026-07) | subsystems/layout.md §6 | ONE seam: FluentGpu.Animation.ScrollIntoView — Bring (nearest scrolling ancestor), BringInto (explicit viewport), ScrollTo (the write half, for callers whose destination comes from a layout MODEL rather than a realized node, e.g. a virtualized list scrolling to an unrealized index). alignmentRatio NaN = minimal scroll; animate writes PendingTarget* + RenderContext.ArmScroll for the phase-7 chase, otherwise it snaps Offset==Target and applies the -offset content transform in the same frame, arresting any in-flight chase or fling. Hand-rolled copies of this idiom are the superseded form. LyricsView.ScrollActiveIntoView is a documented exception (velocity-continuous re-targeting with bespoke spring constants the seam does not model). Gate: gate.scroll.bring-into-view. |
| Repaint damage / partial repaint into the persistent canvas (LANDED 2026-08) | subsystems/gpu-renderer.md §13.1/§13.1a (the contracts + the route policy) — the FrameInfo seam type that carries them is subsystems/pal-rhi.md; the publisher's union-forward carry is subsystems/threading-render-seam.md; the gates are subsystems/validation.md | RepaintDamageRegion = up to 16 accumulated world-space float-DIP rects (pairwise disjoint; least-waste merge at capacity) or a forced full repaint naming a RepaintFullReason; IsEmpty means no rects and no forced-full reason, so "nothing changed" and "repaint everything" can never be confused. The device picks one RepaintRoute per PRIMARY submit: FullDirect (the permanent safe harbor — byte-identical to the pre-§13.1 straight-to-back-buffer path; taken on >60 % coverage, a forced-full region, a size/scale/clear change, or a replay-unsafe stream), FullIntoCanvas (rebuild the canvas so the NEXT small-damage frame is partial-eligible), or Partial (clear + replay ≤4 ReplayRects, or 0 rects = blit the retained canvas). Replay rects are clamped to the target and re-disjointed on the device pixel grid after the round-OUT; the layered route supports up to 2. Damage is CLEARED per rect — never LoadOp.Load over the damaged region (the DrawList assumes a cleared base, so a load-preserve double-blends). Decode-time culling with per-kind, shader-derived halos is a correctness requirement, not an optimization. Replay safety, including the admitted blur/edge-fade and layer-disjoint stencil cases, is owned by gpu-renderer.md §13.1a; acrylic, stencil/layer nesting, unbalanced scopes and unknown/truncated ops fail closed. canvasValid is ONE ledger for the ONE canvas, with per-frame self-heals (instance-bank overflow, uncovered publish gap, size/scale/clear change, device re-init) and a FrameInfo.DrawListHash fingerprint that checks the 0-rect route rather than trusting it. Route parity: a canvas frame must be bit-identical to the FullDirect render of the same state — pinned by FluentGpu.WindowsApp --repaint-identity. No speed multiplier is canonical: the floor is the full-surface blit and the hardware measurement is pending. |
Browser-style app zoom (ZoomLadder + WindowDesc.Zoom / IPlatformWindow.Zoom/SetZoom) (AS-BUILT 2026-08) | subsystems/pal-rhi.md §1.2 (the effective-scale contract + the Win32 raw-vs-effective site table; the seam members in §1) — the ladder type is FluentGpu.Foundation.ZoomLadder; the display ambient Viewport.Zoom and the app relay FluentApp.Zoom/SetZoom/ZoomChanged are usage surface (docs/guide/app-zoom.md) | Canonical value: IPlatformWindow.Scale is the EFFECTIVE scale = OS per-monitor DPI (dpi/96) × app zoom. WindowDesc.Zoom (trailing positional, default 1f) seeds it; SetZoom(float) (ZoomLadder.Clamped) changes it live and requests a paint — the host's per-frame EnsureSize turns any Scale change into a full relayout (the same route as a DPI hop), and WM_DPICHANGED re-derives the product so zoom survives a monitor hop. Zoom is DISCRETE — Chromium's ladder [0.5 … 2.5], Min 0.25 / Max 5 / Default 1 — because the raster caches key on quantized device scale (gpu-renderer.md §5.1 + §13.1 edge list). v1 drives the PRIMARY window only (detached windows: a documented non-goal). |
InputHooks.ZoomWheel (Ctrl+wheel zoom ordering) (AS-BUILT 2026-08) | subsystems/input-a11y.md §7B (the dispatch ordering + the pinch-synthesis carve-out) | Canonical value: in InputDispatcher's Wheel case: element OnPointerWheel first-refusal → on Ctrl, InputHooks.ZoomWheel: Func<float, bool>? with the signed device WheelNotch (>0 = zoom in; returning true consumes the notch) → viewport scroll (ScrollInputRouter.Wheel). Null hook (the default) = behavior unchanged; the Win32 backend consumes Ctrl + hi-res/touchpad wheel as pinch synthesis before dispatch, so the hook sees detented mouse wheels only. |
Content-driven window move (IPlatformWindow.BeginSystemMove + InputKind.WindowMoveSizeEnded + InputHooks.WindowBeginMove/WindowMoveSizeEndedObserved) (AS-BUILT 2026-09) | subsystems/pal-rhi.md §1.2 "Modal loops" (the seam + the Win32 posting/end guarantee) / subsystems/input-a11y.md §3 (the event + the hook pair) | Canonical value: bool BeginSystemMove() (default false) starts the OS interactive MOVE loop from the current pointer for a chromeless window whose CONTENT is draggable (the pop-out video) — the content stays HTCLIENT. Call only while the primary mouse/pen button is held; ASYNCHRONOUS (Win32 enqueues a PointerCancel for the captured contact, then POSTS WM_NCLBUTTONDOWN+HTCAPTION — never SendMessage, never the undocumented SC_MOVE+HTCAPTION syscommand); false = nothing started (fullscreen, no held primary button, closed, no backend loop). true ⇒ exactly ONE InputKind.WindowMoveSizeEnded = 15 follows (every WM_EXITSIZEMOVE raises it, and Win32 raises it itself when the OS returned without entering a loop). Host-wired as InputHooks.WindowBeginMove: Func<bool>? + event WindowMoveSizeEndedObserved (dispatcher OnWindowMoveSizeEnded); headless records BeginSystemMoveCount. Consumer: MediaPlayerElement.DragMovesWindow (a press on the picture that travels past InputDispatcher.ClickSlopPx). |
TimerHandle.RestartIn / TimerHandle.NowMs (computed-deadline one-shot) (AS-BUILT 2026-09) | subsystems/reconciler-hooks.md §0bis (timing hooks) | Canonical value: UseTimeout's handle re-arms for a COMPUTED delay (RestartIn(ms), generation-guarded exactly like Restart) and exposes the host timer clock it schedules on (NowMs = HostTimerQueue.NowMs — the headless frame clock or the monotonic wall clock; 0 without a host). For a pure time-based policy that owns its deadlines (the media chrome's PlayerChromeVisibility.NextWakeMs): feed it NowMs, keep ONE timer armed at its next wake. |
3. Superseded / archived
| Doc / claim | Status | Canonical replacement |
|---|---|---|
archive/dsl-aot-toolchain.md (whole file) | Archived — historical only, contains a known-illegal DepKey layout | subsystems/dsl-aot.md |
foundations.md §1.1 handle layout (the earlier gen-24/kind-8 form) | Corrected in place | {u32 index, u32 gen} (this index, §2) |
| Blanket "no ComWrappers / hand-vtable both directions" (was in README P2, architecture-spec bet 2 + P2 + §ComWrappers, foundations) | Corrected in place | Tiered COM ruling (this index, §2) |
| Single-thread 13-phase loop as the shipping model | Re-labeled "build step 1" via ⊳ banners | Render-thread seam (this index, §2) |
hardened-v1-plan.md §7 + dotnet10 §amendments checklists | Open backlog — apply forward into the deep sections of architecture-spec.md/foundations.md/gpu-renderer.md as those sections are next edited | tracked here |
4. Keeping this true
- When you change a canonical value, edit it here first, then in the owning doc, then run
check-canon.ps1. - The gate excludes
design/archive/. To intentionally mention a superseded form in live prose (e.g. to explain a correction), put<!-- canon-allow: <reason> -->on that line. - New cross-cutting contract? Add a row to §2 with its single owner before two docs can disagree about it.