TODO
Work on this framework that is known about and not done, organised by what v1 means: an
application built on v1 does not need a major refactor, and later releases add to the abstractions v1
already has. So the first question about an entry is not how big it is but whether landing it after v1
would break an application that compiled before. Most of it is what the ports turned up — what
the text editor found, and what
the designer found — where the gaps each port
closed, and the reasoning behind each, are recorded — and, since October, what the applications generated from the
template wrote in their own docs/framework-notes.md. Since 2026-10-22 each milestone of an application ends in a
retrospective in its docs/retrospective.md, and its numbered proposals come here,
under Found by <application>, each linking back to the section it came from.
Keep an entry short enough that it does not need editing, and delete it when it is done rather than ticking it. An entry whose fix belongs in a sibling repo says which one; the ones under Upstream cannot be fixed from here at all. An entry moves between sections when the answer to would landing it later break an app? changes, and says why.
A ruling carries its reason. Anything recorded as ruled here, in components.md, in threading.md or in v1.md says why in the same place: the constraint it comes from, the evidence that decided it, or the cost it knowingly accepted. A ruling with no stated reason is an idea that sounded good at the time, and a later reader cannot tell the two apart, so it has not been ruled yet. When the reason stops being true the ruling is re-argued, not kept for being old. A ruling that turns out to have been wrong is corrected in place and says what corrected it, as the capture rule’s entry does for components.md.
Two labels keep the difference visible. Ruled means the user weighed it and gave, or accepted, a reason. Accepted as recommended means a recommendation was adopted without discussion: it is sound as far as its stated reason goes, and has not been examined beyond that. Neither is permanent, and the second is the one to re-check first when something depends on it.
Blocks v1
Landing any of these after v1 would break an application that compiled before, or change what an application already depends on. Ordered as v1.md orders the work.
-
Geometry is written in
Lengthand read back in four kinds of px (mostly upstream,vexelray-gui). Audited in units.md (2026-10-05), from the text editor’s FN-20. There is no public em basis, so eleven sites convert by hand and three of them are wrong (SplitPane.size,SplitPane’s drag limits,Tooltip’s gap). Every one is hidden whiledpiis pinned to 1, and surfaces when E4 lands. Why this blocks v1:SplitPane.sizeDp/onResize(Consumer<Float>),Table.columnWidthand theNodeLayoutrecord’s components are public. U6 is ruled and built (2026-10-15): em is the node’s own text size and rem the root, each caller of the old em having moved to rem first, so nothing moved. The rest of the recommendation (U1–U5, U7–U9) is not yet ruled. -
The container now gives a component a thread and a mailbox; what is left is the colour rule.
Shell.lanes()owns the application’s threads and the generated wiring puts each@Componenton one of them with its mailboxes, its wake and its drain-then-stop —Lanesin-core(pure JDK, so the container stays testable with no GPU) andPlacementin-shell(a mailbox is atchung’s, and that edge already falls there). The upstream half landed with it:Guitakes both lanes rather than building anewCachedThreadPoolwhatever it is handed, and closes only the lanes it built itself.**Why this blocks v1:** a rule that rejects code v1 accepted is a breaking change, so the copier and capture halves, the message-graph checks and supervision land before v1 or behind an opt-in. They also decide what a component owes the rest of the application, which is how every component is written. **Nineteen of [threading.md](/vexelray-framework/threading.html)'s rules are now held.** Eight came from these two objects, and none of those promotions needed the processor — they needed something to *be* the rule; four remain the processor's (2026-10-02). The `upstream` column is down to one entry, because T1.2 and T1.5 turned out to be the same change. **What is actually left**, and it is the part a runtime object cannot hold: the colour rule's copier and capture halves (T2.4, T2.5 — T2.1–T2.3 are the processor's now, and held), the message-graph checks (§4.1, §4.3–§4.5), and the rest of supervision (§6.3). A lane that has stopped draining is now noticed and named (`Watchdog` and `Lanes.busy`, see *Build the liveness guarantee*), but on a timeout only: `Overrun` surfaced per grouping is still the better signal. Plus the handler lane's bound, which waits on a census of what still blocks on a handler rather than on any one known blocker. **Ruled 2026-09-28: the component model — multithreaded by default — is a design seam frozen at v1.** Groups, lanes, channels, loss classes and supervision are not deferred behind the version number: v1 ships them declared, checked and frozen, or it does not ship. That makes the declaration side a v1 build item and not a later feature, and it makes the eight decisions in the threading audit (the imperative path, public `Shell.place`, `lane`, group = lane, the `@Subscribe` defaults, supervision, the landing rule for checks, the tree) rulings owed before the seam is built. There is no single document for it yet; see the overview entry below. **Ruled: there is a default component lane, and it is not the GUI main thread.** A component that declares no `lane` runs on it, so all such components share one thread by default; `lane` is an optional override, and two components naming the same lane share that one instead. The main thread stays a separate colour (`@MainThread`, never a lane), and the default lane is a lane of its own beside the handler and offload lanes in `Lanes`. The default is the contract, because moving it later changes timing and blocking behaviour without a compile error. This is what makes *multithreaded by default* cheap: the thread count does not grow with the component count, and a plain service costs nothing to make a component. **What it costs:** one slow component holds up every other on the default lane, so isolating one is `lane = "..."`, and supervision has to name a *lane* that has stopped draining, not only a component. **Built:** `@Component`'s `lane` is optional and defaults to `Component.DEFAULT_LANE` (`"<default>"`), which is one shared placement, so one thread. It is reserved by construction: the processor requires an explicit lane to be a name (`[A-Za-z][A-Za-z0-9._-]*`), which the default is not. Nothing to bound: the lane is one thread and each mailbox already has a capacity. Default-lane components may hold each other and not a named-lane one (T2.3). **Ruled: the default lane is under supervision.** The framework watches it always on, as `Stalls` does the main thread, and a lane that has stopped draining is reported by name. That puts the per-lane liveness signal in `Lanes` and `Placement` on the v1 build list, at least for the default lane, and it is the case that made the observer more than optional: one slow component holds up every other on it. **What supervision does is still only report**, and that is frozen: a component's failure never stops the others, a stalled lane is named and not killed by the framework on its own, and richer policies are additive (superseded below: the default action is now not contractual). **Corrected 2026-10-01:** *a component's failure never stops the others* is true of a failure and false of falling behind, because a full FAIL mailbox halts the process on purpose ([components.md](/vexelray-framework/components.html), ruling 5). **Corrected again 2026-10-18, and now false of a failure too:** ruled that every failure is either declared and handled or ends the application, so a handler that throws halts through the fault policy rather than being logged and drained past (components.md, section 10). What stays true is that a *stall* is named and not killed. **Open:** the stall threshold and where it is set, whether lanes an application names are supervised the same way (the mechanism is per-lane, so probably), and what a report looks like to an application that wants to react. **Ruled: the liveness guarantee** ([v1.md](/vexelray-framework/v1.html#the-liveness-guarantee)). A wedged component never stops the window responding, the user always being able to close, or the application ending itself. That turns three things into v1 items: main-thread code may not send on a blocking channel (a processor check, and `Backpressure.BLOCK` from the main thread is the freeze the upstream-topics entry already found), close has a total bound after which the process halts, and the watchdog is a thread on no lane with an explicit `halt`. **To confirm:** the bound (a number and where it is set), and whether a user's close halting the process after it is a default action. It is user-directed rather than autonomous, so recommended yes. **Test it in W2:** a *wedge* button, then assert the window still repaints and takes input, and that close exits within the bound. **Ruled: what a wedge does is a policy the application can override, and the default is not part of the contract.** The frozen parts are the liveness guarantee and the seam that takes the policy, a provider handed back like the look and the input backend, told what happened and acting through a context object (`exit` now, `restart` later), never returning an enum. The default today is report and then exit, matching the bus-fault precedent. The target is a main-thread fallback offering the user a restart. **Realism, unverified:** a fallback on the main thread works for a wedged *lane*, because the default lane is not the main thread, and cannot work for a wedged main thread, where the way out is halt. *Restart* comes in two sizes. Relaunching the process (start a new one from the same command, then halt) is small and needs only what the app already persists. Restarting components in place is not: Java cannot stop a wedged thread, so the old one would be abandoned and could wake later on stale state, and the container builds each part once, so it would need a rebuildable subtree and the tree's depth-first teardown (T6.3), which is unbuilt. Do the first as the target and leave the second to an opt-in policy. A default of *exit* on a stall also throws away unsaved work, so the threshold has to be long and the policy needs a bounded chance to save first. -
The component model’s nine rulings were accepted on 2026-10-01 (mostly as recommended), and part of what they ordered is not built. components.md records them. The imperative path is removed (rulings 1 and 2, done; T1.3 is held). The tree spike is done (ruling 8: zero existing signatures change). The copier’s check half is done (2026-10-02: a
@Subscribepayload must be deeply immutable; the generated copier is parked as additive). Open from them: the capture rule, which needs a decision (see its entry below). -
One seam carries three of the four differentiators, and it is the one not built. A
@Subscribethat generates itsPumpregistration inATTACHand its teardown in theDisposeris at once the actor model’s substrate (above), the extension and macro API, and the reactive half of automation. That third one is easy to miss:Automationis command-in, response-out —click,type,shot,await— so a user-authored macro can drive the application but cannot react to it, and an extension that wants to run when something happens has nowhere to attach. All three want the same thing, which is why it is worth building once rather than three times.**Why this blocks v1:** the [extension test](/vexelray-framework/v1.html#the-extension-test) fails on it twice: elektro-Q and reactive automation have nowhere to attach without it. It is also where a component declares its channels, so it reshapes component code written before it exists. **First slice built (component mailboxes).** `@Subscribe(topic, overflow = FAIL, capacity = 64)` on a `@Component` method registers a mailbox on the component's lane where the wiring constructs it, with the payload type taken from the method's one parameter; the placement's close is the teardown, so there is nothing to generate for it. `@Publishes` declares what a type or method sends on. Held by them: T4.1 (an edge and a sample on one topic, or one topic name with two payload types), T4.3 (a mailbox per method), and T4.7 (main-thread code declaring a send on a `BLOCK` topic). **Not built:** the rest of the seam. A `@Subscribe` that is not on a component (an extension or a macro, which is the whole point of the entry), registration in `ATTACH` and teardown through the `Disposer` for something that is not a placement, and `Fold` (the blocking-cycle check, T4.4, is built, and the graph is exposed as generated data, T4.5, 2026-10-19: the wiring's `BUS_GRAPH`, for the Inspector's bus circuit). The three `@Subscribe` defaults (FAIL, 64, registered at construction and started with the lane) were accepted as recommended on 2026-10-01 and are frozen, capacity included ([components.md](/vexelray-framework/components.html), ruling 5), because changing 64 later changes whether an application dies under a burst. -
Nothing in the model covers work that outlasts a frame, and the stack already named the answer.
kronometer/docs/architecture.md§10 specifies it: “offload(work)remains available for work that is genuinely unbounded — file I/O, network, image decode — moving it to an ordinary executor (its own, never the kernel’s single carrier) and delivering completion as a timeline event.” Specified and unbuilt —offloadappears in no Java source in that repo. The framework wants the same lane, and naming it here is what stops the component model being read as the answer to a question it does not answer.**Why this blocks v1:** the completion path is what application code will call to get a result back onto the frame loop. The pool is the easy half; the way back through the timeline is the API. **A pool does not contradict static placement.** A component is placed statically because it is stateful and ordered; an offloaded task is stateless and unordered, so there is nothing to confine and no sequence to keep. The edge is policed by [the crossing rule](architecture.md#what-may-cross-a-lane-and-what-crossing-does-to-it) — a value crossing a lane arrives as if it had crossed a wire — which makes an offloaded lambda that captures a component's state a compile error rather than a race. Capture is the one case the rule refuses rather than copies, because a lambda closes over a reference and no copier can be slipped in behind it. **Platform threads, and the reason is not the component one.** Blocking I/O is the textbook virtual-thread case, but §3.1 records that `jdk.virtualThreadScheduler.parallelism` is a global JVM property with *"no public per-thread scheduler"* in JDK 25 — so an application that takes Kronometer's 3× baton flag has no second carrier to give this pool, and the obvious choice is the documented deadlock again. That flag is the application's and correctness never depends on it, so the framework's default has to be the one that is right when it *is* set. **The completion path is the content; the pool is the boring half.** A result is published on a `Topic` and folded into a `Cell` by `KronBridge`, or dropped on a queue drained in `FrameStage.APP` — the two doors that already exist, and `APP`'s own list is *"a history to restore, a file to open, a preview to render."* What an offload thread must never do is touch the tree or the timeline in place. **The lane now exists.** `Lanes.offload()` is bounded, platform, and separate from the handler lane; `Gui.offload()` is the same lane reached from a widget. Two lanes rather than one, mirroring the split Kronometer already makes between the precompute pool and `offload`. **What this entry got wrong is worth keeping.** It said `FileActions` called `Files.write` and a blocking `FileDialog.save` *"inline on a handler thread"*, sharing an unbounded lane with click dispatch. It did not: every command there goes through `GuiApp.post`, so the I/O was on the **GUI thread** — which is the right place for the dialog, whose contract requires it, and a worse place for the blocking call than the handler lane would have been. A read from a mount that had gone away held the frame loop rather than one document. Both are now on the offload lane and land back through `app.post`; the dialogs stay where they were. **The lesson is about the inventory, not the editor:** a claim about which thread something runs on was written once, was true once, and was not re-checked when `app.post` moved it. **Still to do**: `kronometer`'s own `offload` remains specified and unbuilt, and the framework does not yet route completions into the timeline — the second door (a `Topic` folded into a `Cell` by `KronBridge`) is unused, and everything that lands today takes the first one. **Found 2026-10-02, reading what `work` may do** (the callback that runs on the offload lane in `GuiApp.offload(work, landed, failed)`). Gaps in what is checked (an `Error` from `work` used to reach neither callback; since 2026-10-18 it reaches `failed`, wrapped in an `ExecutionException`, and a `VirtualMachineError` is rethrown after): - **A send from `work` is invisible to T4.4.** `@Publishes` goes on a type or a method and a lambda has no declaration, so the cycle check cannot see that `work` blocks sending to a mailbox whose owner is waiting on this very job. A blocking cycle through an offloaded task is a deadlock nothing reports. - **The result `T` crosses a lane and is not run through T2.4**, and `landed` and `failed` always return to the main thread rather than to the caller's own lane, so a callback written on the handler lane that captures handler state races. Delivering completion to the owner's lane, or as a message to a component's own `@Subscribe`, would remove the capture question for those two. Only `work` would be left with it. - **Cancellation and interrupt do not reach it** (already listed under *Build the liveness guarantee*): the offload pool is not in a `ThreadGroup`. - **Unverified:** whether `work` mutating a `Node` directly is legal. `Gui.async`'s Javadoc says a result can come back "through a node mutation"; this entry says `work` must never touch the tree in place. -
The wiring is generated; what it does not do yet. The processor writes
<App>Wiringfrom@VexelApp,@Provides,@Component,@Setting,@BeforeFrame,@OnModeand@ConditionalOnType, thevexel-desktoptemplate ships aRecipesconfiguration instead of a hand-written wiring, and-Pacceptancebuilds and drives the result. The resolution rules are the contract — an application depends on which provider satisfies a parameter without ever writing it — so the decisions below are v1 items and the additions are not. Each bullet says which: - Additive, once the rule is written. Most of the framework’s defaults still cannot be replaced. The look, the input backend and the clipboard can — a@Providesreturning one is handed back toShell(see architecture.md, the framework’s own defaults are handed back). So can the icon, which is named rather than provided:@VexelApp(icon), falling back to the framework’s mark (architecture.md, the application’s mark). Window memory, the dialogs and pacing cannot, and the README lists them among the defaults. Each wants a reason to be replaced before it gets a setter; none has one on the stack yet. - Additive, but before a BOM. A starter is not checked where it is compiled.AutomationStarterTestcompiles applications againstAutomationStarterwith the real processor, which covers the one starter there is; the processor is not on-automation’s ownannotationProcessorPaths, so a starter’s library-level checks run only when an application names it. Worth wiring when there is a second starter, and before a BOM. - Ruled and built 2026-10-18: a provider returningnullis passed on and reported once. The generated wiring callsAbsences(-shell), one WARN onframework.wiringnaming the provider and each part built with the null (named in atchung’s logging.md). Why:@Providesalready promised it, an application returning null on purpose keeps working, and a startup error would have needed an “optional” annotation to say which nulls are meant. - Ruled 2026-10-18: types match exactly. A parameter asking for an interface is resolved only by a provider returning that interface, never by one returning a subtype. Why: the two directions are not symmetric. Allowing a subtype later, and only where there is no exact match, turns errors into successes and changes nothing that compiled; allowing it now means an application that adds a second subtype of something a parameter asks for stops compiling on an ambiguity it never wrote. Exact is the one that keeps the door open. - Additive. One round. The graph is taken in the round the@VexelAppis seen, which is every source of a clean build; anything another processor generates in a later round is not in it. - Decide now. Two promises need a method body. A@Providescalling another directly, and T2.5’s capture rule, are both about what code does rather than what it declares. The Trees API can read them; doing so is a decision, since the processor reads declarations is load-bearing in architecture.md’s argument about placement. Found 2026-10-02 while scoping T2.5, and it corrects components.md: capture cannot be made impossible by construction. Offload is anExecutor(Lanes.offload(),Gui.offload()) andGuiApp.offload(Callable, Consumer, Consumer), and Java has no type that refuses a lambda that closes over something. So the choices are a check that reads the lambda through the Trees API with attribution, from a javac task listener (accurate: it resolves each name to a local, a field orthis, and runsShareableon what it finds; and the heaviest piece in the processor, which has never run outside the annotation rounds), or leaving T2.5 as a rule the reader keeps. A syntactic check without attribution is not an option: it cannot tell a captured local from a class name. Not decided; a third option is proposed (components.md, section 10, 2026-10-18): slow work as a declared@Jobwhose result is published on a reply topic, so there is no lambda to capture with and the check reads declarations only. To rule on: adopt it, and failure’s shape. - Before v1, with the component model. Done 2026-10-02: the copier’s check half (a@Subscribepayload must be deeply immutable) and the blocking-cycle check (T4.4). Additive, not built: the generated copier. Done 2026-10-19: the graph exposed as data (the wiring’sBUS_GRAPH, vexelray-inspectordocs/bus-circuit.md). -
vexelray-engineis a second composition root, andGuiAppis the first. The new module (in../vexelray) exists because “six demos each carry a copy of eighty lines of instance/device/swapchain/presenter wiring” — andGuiAppis a seventh, doingnew VulkanInstance,selectGraphicsPresentDevice,new VulkanDeviceand drivingWindowedPresenteritself. Nothing is broken by that refactor: it kept every prior constructor andConfigform deliberately, so the stack still compiles and this repo’s tests pass against it. ButVexelEngine.createresolves anEngineProviderthroughServiceLoader, so ifGuiAppis ever rebuilt on it the stack acquires reachability metadata an application inherits and this framework does not yet aggregate — the open question architecture.md already calls “probably the highest-leverage feature not yet listed.” Nothing to do here until that seam moves; recorded so it is not a surprise when it does.**Why this blocks v1:** which root owns device creation decides what reachability metadata an application inherits, and so the build shape. Nothing to write until that seam moves, but the decision precedes the BOM. -
Gui’s topics arestatic, so one bus can carry one tree (vexelray-gui-core). Every instance subscribes to the samevexelray.gui.mutations, so two trees on one bus each receive the other’s mutations — into a mailbox bounded at 65,536 withBackpressure.BLOCK, drained only while that tree is being presented. The secondGuifills and then blocks the first one’s node setters for good: a freeze after tens of thousands of edits, with nothing thrown and nothing logged. This framework shipped it for three commits by puttingModalsonShell.bus(), and the designer found it by putting a second window there and watching the viewport stop marching.**Why this blocks v1:** it caps one fabric per application, so `Shell.bus()` cannot promise that a second window joins it. Either the fix lands upstream first, or v1 documents the ceiling as part of the contract. **A third time, 2026-10-07:** `Popout` did `new Gui(host.bus())`, the calculator's settings window copied that line, and the main window came up drawing the *settings* tree from its first frame, with nothing thrown (calculator FN-2). `Popout` now gives the popped-out tree a bus of its own (`vexelray-gui` `a93a661`). Each new second window was another chance to copy the wrong line, so since 2026-10-11 the framework builds a named window's tree itself (`Windows.named`, on a bus of its own and the application's lanes), and no application on the stack makes a `Gui` for a second window any more. Constrained rather than fixed: `Gui(Atchung)`'s javadoc now says which things may share a bus. **It is also the ceiling on one inspectable fabric per application**, which is the point of having a bus at all — a second window cannot join it. The fix is topics named per instance rather than per class, and it is a real change in `vexelray-gui` rather than a line here. -
What
Shellhands over is the seventh surface, and it bounds the other six.ShellreturnsGui,GuiApp,KronoGui,TitleBar,SettingsandAtchung, and since OctoberComputeQueue(Shell.computeQueue) anddev.vexelray.os.Icon(Shell.windowMark);Windows,WindowandDialogsare the framework’s own and hand overGui,TitleBarandModalin turn (WindowMemoryandCloseRequestare no longer handed over at all, since 2026-10-11). So an application holdsvexelray-gui’s andatchung’s types directly and v1 is only as stable as they are. Either those repos freeze what is exposed, orShellstops exposing it — a wrapper per type, or fewer accessors. A decision rather than a task, and it wants making per accessor rather than once. Every side of the edge is the author’s own, which makes freezing cheap and makes not deciding the only expensive choice.**Ruled 2026-09-28: `Gui` is frozen** — the subset applications use, with the remainder marked experimental. `vexelray-gui/docs/plans/gui-decomposition.md` reports all seven decomposition steps done, and the last app-facing removal from `Gui` was `onCaretHit`/`onCaretDrag` on 2026-08-14; nothing local calls them. Still to do: classify the ~100 methods, and give `vexelray-gui` a marker of its own, since the framework's `@Stability` sits above it in the dependency order. **Ruled 2026-09-28: hide or delete what nothing needs; delete is preferred.** The `Shell.dialogs()` of the time was deleted: no caller, and it was not an injectable root anyone had a use for (the name came back on 2026-10-11 for something else, the `Dialogs` root). Open then: `memory()` was an injectable root in the processor's table, and the text editor port had needed `WindowMemory` injected (architecture.md, *what porting the text editor found*), so *no caller* was partly the deleted witness. `app()` could not simply go: `Driver`, in a separate module, reached the window's `WindowControls` through it, so the choice was a narrower accessor for exactly that, or an explicit host seam. **Done 2026-10-11, on that ruling.** `memory()` is deleted, accessor and root: the windows that used it (the calculator's settings, W2's tool window) are remembered through `Windows` now, and a census of every application on the stack found no other caller. `Driver` reaches the windows through `Windows.views()`, the narrower accessor, and not the `GuiApp`. **Ruled 2026-10-11: `app()` stays, as the device and not the windows.** Reason: the same census found every remaining use of `GuiApp` is the device or a dialog — Pix's and Vexplore's textures, the fluid and rigid simulators' viewports and compute, and the dialogs of the text editor, Vexplore and Pix — and none is window management, which is all `Windows` now covers. The device is the main thread's by rule, so a recipe taking it is `@MainThread` correctly, and a narrower type for it would be the same boundary under another name. The dialogs are the `Window` seam entry's own bullet and move off it there. They have (`Dialogs`, the same day), and so has the one window-management use the census missed (Pix's viewer closing the main window, now `windows.main().close()`), so what is left behind the `GuiApp` is the device and nothing else: textures, viewports and compute. -
Witnesses: the abstractions have been drawn from one application shape. The three ported applications were deleted on purpose, which was right, and for a while left one generated counter-style application validating everything. (Since then the template writes a text editor, and Vex, the calculator, Vexplore, Pix and the simulation demos are generated from it; their findings are below. Most are one-window-plus-dialogs desktop applications; the calculator’s settings and Vexplore’s viewer are each one second window, and W1 is the one with several.) One component is not a census applies to the framework itself. Wants two or three generated applications that differ where it matters — several windows on one device, component-heavy with real placement, long-running with nothing to settle — each expected to find something, and each finding written up in architecture.md as the ports were. After the component model, since that is what they most need to exercise.
**W2 (component-heavy) and W3 (long-running) are built** and pass: [what W2 found](architecture.md#what-the-component-witness-found) is mostly confirmation plus two observations, and [what W3 found](/vexelray-framework/architecture.html#what-the-long-running-witness-found) is two real findings, both filed below. **W1 (several windows on one device) is built, 2026-10-11, and passes**: [what W1 found](architecture.md#what-the-two-window-witness-found) is what shaped the `Window` seam — named windows, the main window, the dialogs and the close gates — and found an upstream hang that is now fixed (the lost wake under *Upstream*). So all three are built; what is left is depth, not breadth: W2 lacks a wedge that ignores interrupts, which is what would make the halt backstop fire, and W3 is twenty seconds long, which rules out growth per tick and nothing slower. -
Starters, a BOM, and the stack’s reachability metadata. The build shape is a contract: an application’s dependency block and its
@VexelAppare the first things it writes. One starter exists (AutomationStarter), so the abstraction is drawn from a single instance; a second starter is the test of it. The BOM and the aggregated native-image metadata are what make one dependency enough, and the metadata is what architecture.md calls “probably the highest-leverage feature not yet listed.” After the component model, because what an application inherits depends on what the stack turns out to reach.**Ruled 2026-09-28: v1 may ship native on Windows only, provided adding another OS later needs no change to an existing application.** That makes where the metadata lives a v1 decision. It must be carried by each backend's jar (GraalVM merges `META-INF/native-image` from every jar on the classpath) or by a starter, never by a file in the application, or every new OS is an edit to every app. The per-OS backend selection in a starter has to meet the same test. **Corrected 2026-10-05:** this said no generated application had ever been built as a native image. The text editor, vexplore and mainframe now all are (Windows, GraalVM 25, `-Pnative` in each app), which is exactly the case the rule above warns about: each carries its own metadata and flags, see the next entry. `-acceptance` itself still stops at a JVM build. **Refined 2026-09-29: Windows is the v1 requirement; macOS is wanted but not required; Linux is in scope and will become load-bearing, but is not a priority until a distro is chosen and gives it a real build target.** The additivity test above applies to all of them, so what v1 must get right is where the metadata lives and how a backend is selected, not the number of backends. **Ruled: every native extension is cross-platform in theory, or wrapped in a facade for the future.** Platform-specific code never appears in a public signature or in an application's build. The stack already has the shape (`NativePlatform` behind a `ServiceLoader`, `InputBackend`, `ClipboardBackend`), so the rule is that a new native piece follows it and that an existing one which does not is a v1 fix. **To audit, unverified:** which native pieces have no facade today. The reachability report names FFM bindings in `vexelray-os-windows`, `tactroller-clipboard` and `tactroller-linux`, a native-file-dialog loader that extracts a library from a jar, and bundled `natives/` for image decoding. -
Three applications carry their own native-image metadata and flags; it belongs upstream. Found 2026-10-05, building
text-editor-vexel-demo,vexploreandmainframeas native executables (about 30 to 40 s each, one 27 to 43 MB.exe, and each ran). Every one needed the same things written into the application: areachability-metadata.jsonrecorded by running the app on a JVM under the tracing agent,-H:+ForeignAPISupport,--enable-native-access=ALL-UNNAMED, and (mainframe, forArena.ofShared)-H:+SharedArenaSupport. The first build of two of them failed at runtime, not at build time, on a missing FFM descriptor (MissingForeignRegistrationErrorinUser32.<clinit>) or onArena.ofShared, so what is missing shows only when the path runs. What the agents say belongs where: -vexelray-os-windows: theWin32Window.wndProcandWindowsPlatformreflection, the FFM downcall and upcall descriptors it binds, and theNativePlatformMETA-INF/servicesentry. -tactroller-windows:WindowsInputBackendandWindowsRawInputHubreflection, theInputBackendservice entry, their descriptors.tactroller-clipboard: its descriptors. -vexelray-gui-nfd: thenatives/windows-x64/nfd.dllresource and its downcalls. Whether its loader, which extracts a library from a jar, works in a native image is not known: the dialogs were never opened (ottermatecannot click a native dialog). - The jars that own them: the shader.spvand text-atlas resources (vexelray-gui-draw,vexelray-text,supirvast), and the jonitables/*.bin, TM4E reflection and signed-jar workaround (a text-highlighting module or the editor’s own starter, not each app). - A starter or the BOM: the-Hflags and--enable-native-access, thenativeprofile itself, and the genericjava.awt,java2d, ImageIO,sun.security.provider.*andawt_enentries. - Already duplicated:vexelray-gui-democarries nearly identical metadata. Consolidate it into the same place rather than a fourth copy. Why this blocks v1: it is the ruling in the entry above, now with evidence: the metadata is carried by each backend’s jar or a starter, never a file in the application, or every new OS or backend is an edit to every app. Today it is the other way round. Unverified, for whoever moves it: the metadata was traced from limited runs, so a path nobody exercised (the dialogs, the clipboard, mainframe’s other shells) may still fail at runtime, and tracing is per application, so upstream metadata wants a trace of each backend’s own test or demo rather than of an app. Since 2026-10-09 the template writes the native build too (-Pnative,-Pnative-release, the two editions, the icon, and areachability-metadata.jsonholding only the entry point and the JDK entries the calculator’s trace saw). That is the arrangement this entry says is wrong, now copied into every new project. It is the honest default until the profiles and flags have an upstream home, and it is small, because the libraries already carry their own metadata. When they move, the template’s pom is the place to delete them from, with the five applications. -
The
Windowseam: built, and one choice left.memory(),app()and one-off dialogs were each a singleton accessor in a world where a window is not, and every one of them had to say which window; adding that later would have changed the signatures applications call. Built 2026-10-11 and shaped by W1 (what W1 found), all of it not main-thread and safe from any thread: -Windows, a root fromGUI.windows.named(NamedWindow...)declares a second window: the framework builds its tree at once (aGuion a bus of its own and the application’s lanes, the look, a title bar) and claims it atATTACH(placement and zoom remembered under its name, the window mark, the bar’s controls and instruments, the clipboard).windows.main()is the main window, whosecloseis quitting.windows.views()is whatDriverreads. The calculator’s settings window, Vexplore’s viewer and W2’s tool window each lost a@MainThreadrecipe (calculator FN-1, FN-2). -Dialogs, a root fromCONFIG: aModal, and NFD’s open, folder and save pickers, parented to the main window and answered on the handler lane. A question asked before there is a window is held and asked once there is; a@Provides Dialogs(orshell.dialogs(...)byWINDOW) replaces the framework’s, which is how a test answers. The text editor’sNativeDialogs, Vexplore’sChooserand Pix’sOpenerwere three copies of one class, each taking theGuiApp, and are gone (Vex FN-6’s dialog half, FN-7).Modalsstays a static, for MainFrame, which is hand-wired. -Window.onClose(CloseGate), a close gate on any window, the main one included: two callbacks rather than gui-core’sCloseRequest, which a test could not construct, so the gate a test calls is the gate the window calls (Vex FN-6’s other half). Once per window, read when the close arrives.Shell.onCloseis deleted; the template and the text editor armwindows.main().onClose(actions::guardClose), and W1’s palette asks before it closes. -memory()is deleted, accessor and root;app()stays, as the device (the ruling under whatShellhands over above).**Left: reopening what was open.** `WindowMemory` records whether a window was open (`wasOpen`), and nothing uses it, so a window open at quit is closed at the next launch. A choice to make deliberately, per window (`NamedWindow` is where it would be said), rather than a default to inherit. Additive, so not v1-blocking on its own; it stays here because it is the seam's. -
Spike the extension test. v1.md: for each row of the extension table, a throwaway prototype against the API as it stands, and the number of existing signatures it forces to change recorded here. Candidates: the second window, a Linux backend,
@Subscribe, supervision, a second starter. Zero is the target; anything else is fixed before the freeze. Do it with the witnesses, since both need the same applications.**Done 2026-10-02: the tree, 0.** A throwaway `@Component(parent = ...)` (an attribute defaulting to none; a child shares its parent's lane unless it names one; the parent is built first; a non-component parent or a parent cycle is an error) changed no existing signature and none of the 67 existing processor tests; `Shell`, `Placements`, `Lanes` and the runtime were not touched, since a lane is a string. Not spiked: child-before- parent shutdown, which falls out of reverse construction order but was not run, and supervision naming a subtree. The other rows (Linux backend, supervision, second starter) are still to do. **The second window, 2026-10-11: built rather than spiked, 2.** `Windows` landed for real (W1), so the count is of the actual change: two existing signatures went, `Shell.memory()` and `Shell.onClose(Consumer<CloseRequest>)`, both deliberately and before the freeze, each replaced by a per-window form (`Windows`, `Window.onClose`). Everything else was additive: a new root, new types, no recipe signature forced to change. -
Build the liveness guarantee. Designed in architecture.md, contract in v1.md. Landed, all tested with no GPU: each component thread in its own
ThreadGroupwithLanes.interruptLane; a per-lane busy gauge (Lanes.busy, set around everyPlacementdelivery, so parked is idle and inside one delivery too long is a stall);Watchdogin-core, a thread on no lane that reports each stalled delivery once and ends the process withRuntime.halt(status 1) when a deadline passes;Disposer.beforeClose, which arms a total bound on shutdown; andLivenessPolicy, handed back withshell.liveness(...)or a@Providesreturning it, with a default for every part. Defaults, not contract: stall threshold 10 s, shutdown bound 10 s, exit grace 30 s (the time a close gate’s save prompt has before the process is ended anyway), and on a stall report then exit. All three durations are the policy’s to override.**Still to do:** the main thread is not watched, on purpose. It legitimately blocks in a modal native dialog for as long as a person takes, and a halt fired at someone choosing a filename is worse than the wedge, so it wants a heartbeat that knows a dialog is up, from the frame loop. The processor check that main-thread code sends only on channels that never block is built (T4.7) for declared sends only; see the `@Subscribe` entry. Cancellation tokens on `Lanes.offload()`, and the handler and offload pools are not grouped. `Context` has `exit` and `interrupt` and no `restart`. Verify the group sweep under native-image, untested. W2 gets the *wedge* button and the assertions that the window keeps painting and close exits within the bound. -
A look is colour; shape, density and type are each widget’s (upstream,
vexelray-guiandvexelray-text). Found by Vex’s retheme to a design mockup (FN-14 to FN-17): every colour landed through the palette, and nothing else did. Corner radii, border widths, padding, row heights and text sizes are literals in each widget, some repainted on every state change so they cannot be restyled after construction, and a role override is global (a quietLINEfor the cards quietens the dialogs too). The atlas is baked from two Noto faces, with no third face, weight, slant or tracking, and it lacks glyphs applications need (the calculator spells≤as<=, FN-3).Spancarries a colour and no style. Vexplore’s FN-6 is the same finding from the palette side. Why this blocks v1: how an application declares its fonts is build shape, which v1 freezes;Node.font(int)is the one text-styling call;Spanis a record, so adding a style component later breaks its constructor and every deconstruction pattern; and turning widget literals into token reads moves every application’s pixels. Shape and density tokens onThemeare additive (an interface with defaults); the fonts andSpanare not. -
A legacy sweep: v1 keeps nothing for backwards compatibility. The second v1 condition, in v1.md. Known candidates so far, each a question and not yet a finding: the prior constructors and
Configforms the engine refactor kept on purpose;--profile, which parses and nothing honours;shell.wake(gui::onWork), which duplicates whatGuiAppalready does; theVexelApplication.runoverload taking a window factory, which looks like a test seam;Gui’shasPendingWork(no caller),keyRoutes(no caller) and theframeOwedoverlap;Modals’ one-argumentinstall, documented as a library default; the stalesealed ... permitsin vexelray-gui’s architecture.md §6; and the deliberately not bounded yet handler lane.**The imperative path is gone (ruled and done 2026-10-01, [components.md](/vexelray-framework/components.html) rulings 1 and 2):** `@Subscribe` is the one way, `Shell.place`, `Placement`'s constructor, `start` and `subscribe` are package-private, and `Placement.superseded()` is deleted. What is left of the sweep is the list above, and it binds the siblings `Shell` exposes. -
The public surface has not had a freeze pass. When the rest of this section is done: read every public type and ask whether anything would be renamed or restructured today, fix it, then mark what is frozen. This is the step that turns nothing known is wrong into 1.0. It cannot start early, because it is meaningless while something above is open.
Can land after v1
Additive: a constructor parameter with a default, a test, a deletion that changes no behaviour, a new module behind a seam that already exists. Worth doing, and none of it waits for the freeze.
-
Relaunchhas only been seen under the JVM (-shell, 2026-10-19). In a native image it restarts the image with its arguments and never callsManagementFactory, but the call is still reachable, so build a native release and restart it in debug mode once. The JVM path rebuilds the launcher from the input arguments, the class path andsun.java.command, becauseProcessHandle.Info.arguments()is empty on Windows; the process’s ownGetCommandLineW(vexelray-os already reads it, inCommandLine) would be exact on both, behind aNativePlatform.commandLine(). -
A light/dark switch in the title bar, for an application that declares both looks (
-shell’sAppearanceand title bar, probably as aWindowInstrument;-template). Asked for 2026-10-21, after the scheduler retrospective. - Ruled 2026-10-21: the application authors both themes, and the framework derives neither. Reason: writing a design system takes under five minutes, and the two hand-tuned pairs on the stack (Palette.DARK/LIGHT, Vex’sLook) show that a light theme is not a reflection of a dark one. The light page sits at L 0.975, where a mirror gives 0.809. The accent goes from 0.6925 to 0.56, where a mirror gives 0.31. Shadow alpha goes from 0.55 to 0.18. A derived theme would be a guess that somebody then tunes by hand. - So the button appears only whenAppearancecarries a second theme, and an application with one theme has none.AppearanceisEXPERIMENTAL, so a new component behind a wither is additive. - The choice is remembered beside the zoom (WindowMemory). It applies to every window that took the application’s theme, never to one with its own (applyTo’s rule). The swap isGui.theme, which fades under aPaintMotion, so nothing changes in one frame. - The catch is a colour kept as aColorrather than aRole, because a retheme cannot reach it. The template’s ownLook.EMPHASISis one, and so are Vex’s scope colours, which is why Vex’s theme menu only takes effect at the next start. The template should make each of its own colours a role, and test each role’s contrast against the page in both looks. -
The camera records as well as photographs, for a person and for ottermate (
vexelray-gui-core’sWindowInstrumentandWindowControls,vexelray-gui-nfd’sSaveScreenshot,vexelray’s presenter,ffmpeg-wrapper, ottermate). Asked for 2026-10-21, after the scheduler retrospective, which had to build a slow-motion switch, burst screenshots and a frame-step script to check that nothing changes in one frame. - What the user sees: the camera opens a menu (WindowInstrument.menu, the seam the automation plug already uses): Take screenshot, and Record video to file…. While recording, the mark and the tooltip say so (Live) and a click stops it. Later, Take stills of contentful redraws. - Recommended, not ruled: one frame tap, three sinks. A tap hands every frame the window presents, with its present time, to a sink. The sinks are a video file, a still per redraw, and ottermate’sfilm(largest step between frames, the retrospective’s spike detector).capturecannot be the tap: it forces a render and reads it back synchronously. A recorder must not cause frames, and must not stall the loop. So the readback copies into a ring of staging buffers and is mapped a frame or more later, on a worker. That is a presenter feature invexelray, inside the frame budget. - Decided 2026-10-21: AV1, in Matroska/WebM. Why it fits (the recommendation’s reasons, not yet weighed): it is royalty-free, with BSD-licensed encoders, where x264 is GPL. It has coding tools made for screen content, palette mode and intra block copy, which are what flat UI and text need. And Matroska stamps every frame with its own time, so a loop that presents nothing while idle is recorded as it happened, with no repeated frames. The pinned build already held the encoders and the Matroska muxer, so no library was added. - Built 2026-10-21: the encoder, SVT-AV1 in real-time mode, with the GPU’s as an option.ffmpeg-wrapper’sFfmpeg.record(aRecording: RGBA8 frames with their times in, AV1 in.mkvor.webmout) andvexelray-codec-video’sRecorder(offered frames from any thread, never waits, encodes on its own thread, drops and counts what it cannot queue). At 1080p it takes about 7 ms a frame in software and 4 ms with NVENC, on the Core Ultra 7 255HX laptop.Encoder.HARDWAREtries NVENC, then Quick Sync, then AMF, and falls back to SVT-AV1. libaom was ruled out as too slow. rav1e was not measured. Windows is built and tested. The Linux build script compiles the encoder in too, but has not been run on Linux yet, and VAAPI is not wired, so Intel and AMD GPUs on Linux record in software. Whether SVT-AV1’s screen-content detection engages on a real window is not measured. - Decided 2026-10-21: a window resized while recording stays on one canvas. The recording is opened at the screen’s size, and each frame goes at its top left, unscaled, on black. A larger frame is cut to fit. AV1 can change size from frame to frame, but Matroska’s track header holds one size and players differ on which they believe. The canvas plays everywhere. Its black added 4% to the file, but the encoder works on the whole canvas, so CPU follows the screen’s size, not the window’s: about three times as much for a quarter-screen window. The tap hands over each frame at the window’s size and the screen’s size at the start, and the recorder handles the rest. - Built 2026-10-21: the frame tap, the camera’s menu, the save dialog and arecordverb. A person can start and stop a recording, and so can a script. What is where: -vexelray:WindowPresenter.tap(FrameTap), andFrameTapRing, shared byDxgiPresenterandWindowedPresenter. Each presented frame is copied, in its own command buffer, into one of three mapped host-cached buffers, and handed over in order once its fence has signalled (vkGetFenceStatus, which never blocks). A frame that finds every buffer held is skipped, not waited for. The swapchain now asks forTRANSFER_SRC.PresenterTapTestcovers the swapchain path on a real window. -vexelray-gui-core:WindowControls.tapandworkArea.WindowRecorderis the seam a codec module fills throughServiceLoader, so the GUI links no encoder.WindowRecordingholds the process’s one recording: start, stop, the camera’sLIVE, a file finished when its window closes, and a shutdown hook.WindowInstrument.cameraand its filled-lens mark. An instrument’smenuis now told its window. -vexelray-gui-nfd:SaveRecording, the menu (Take screenshot…, Record video to file… or Stop recording) and the save dialog.SaveScreenshot.instrument()returns it when a recorder is on the class path. -vexelray-gui-automation:record <path>,record stopandrecord. There is no ottermatefilmsink yet. -vexelray-codec-video:WindowVideoRecorder, encoding BGRA straight from the mapped buffer, with the last frame held until the stop (Recording.repeat). -ffmpeg-wrapper: a cluster closes every second and every packet is flushed, so a killed process leaves a file that plays.Checked on the video player under ottermate on Windows (DXGI): 1080p video in a 1280x760 window, 182 frames in 7.3 s, none dropped. The frame recorded at the moment of a `shot` matched the screenshot at 38 dB. Stopping from the menu, closing the window and killing the process each leave a file that plays. **Not run on Linux.** The native save dialog was not driven, because a background run must not raise one at the person, so *Record video to file…* has been clicked by no one yet. `-Dvexelray.vulkan.validation` was not available, so the copies have not been checked by the validation layer. - **Asked 2026-10-21: a stepped recording.** Kronometer drives the clock while recording, and each presented frame advances logical time by exactly one video frame, however long it took to draw, read back and encode. The window may stutter for the person watching it, but the file plays perfectly smoothly. `KronoGui.tick(Dur)`, "for a scripted or headless run", is the seam, and a stepped frame can afford a synchronous readback because the frame budget no longer applies. - **Prerequisite: one clock for everything that moves.** `InputDispatcher` steps the wheel glide and the focus ring's fade from `System.nanoTime()`, not from the kernel, so a stepped recording would play them at the wrong speed. The scheduler hit the same thing in slow motion (its FN-7, and the retrospective's global time scale). What is drawn reads the frame's time. Key repeat and double-click stay on the wall, because they measure a hand. - **The catch is input.** Logical time drifts from the wall, so a person's own timing is warped: a pause in which twenty slow frames were drawn plays back as a third of a second. A script driving in logical time (ottermate) does not have this problem. - **Recommended, not ruled: idle runs on the wall, motion runs on the step.** While nothing is animating, logical time follows the wall, and Matroska's per-frame times keep the pause as long as it was. While something is animating, time advances one step per frame. A person's pauses survive, every animation is smooth, and the only cost is that input arriving mid-animation waits for the next stepped frame. - **Open: when the stills mode takes a still.** A *contentful* redraw is a presented frame whose pixels differ from the last one. Many animation frames will qualify, so it wants a cap or a minimum step. -
The template’s navigator marks its row actions with text glyphs (
Navigator.java:"›"for Open,"»"for Show this folder). Since 2026-10-20 an icon the suite’s set lacks is an application’s own baked set (src/main/icons, the pom’sapp-iconsprofile,vexelray-gui-icons/README.md), and Vex’s navigator already moved:Icon.CHEVRON_RIGHTand its ownTextEditorIcon.MAKE_ROOT. The template should follow, which means either shipping art in every new project or amake-root-like icon joining the suite’s set; which one is the question. The rest of the stack’s glyph-icons are listed by app: the inspector’s fan (a drawing in three metals, animated, so not a one-colour field), Vex’s and the template’s unsaved dot, and PictureViewer’s Loop, which borrowsREFRESH. -
The template’s
.gitignoreis five lines (-template,vexel-desktop/files/gitignore):target/,capture*.pngand three IDEs. It misses.claude/, which every sibling repo ignores by hand, along with the JVM’s crash and profiling dumps, the native build’s leftovers besidetarget/, and OS files. Found 2026-10-20 invexelray-scheduler, whose root.gitignore(49a16b5) is the one to copy: sections per source,!**/src/**/target/asvexelray-guihas it, and captures by name rather than a blanket*.png. Its Lean section is that repo’s own and can stay there. The applications already generated (mainframe, Vex, the calculator, Vexplore, the inspector) keep their copies until someone updates each by hand. -
VEXELRAY_AUTOMATIONopens the socket but does not set the log mode (atchungLogConfig, or-shell’sLogs.start).Driverreads the variable since 2026-10-18;LogConfig.systemSettingmaps theautomationkey to an environment variable namedAUTOMATION, so a run asked for only through the environment logs in DEV mode rather than AUTOMATION. Decide whether the variable should also make a release binary, which has no socket, log as loudly. -
Settings and the session are one file per application, written whole, and nothing stops two processes overwriting each other (
vexelray-gui-core:Settings,AppHome,WindowMemory). Found 2026-10-05 planning the vexelray suite (the editor, vexplore and mainframe in one install, each spawning the others, one new window per spawn).Settings.save()writes every key it holds, from memory, to~/.{app}/settings.properties: atomic and thread-safe, but only within one process.WindowMemorykeeps window placement and zoom in that same store, and an application keeps its open files there too. So two windows of one app each save their own stale view of every key and the last to close wins, and both write the same fixedsettings.properties.tmp, so concurrent saves can clobber each other’s temp file as well. - Ruled 2026-10-05: only an instance launched without arguments reads or writes the session (window, folder, open files). One started with paths, as a spawn from another app is, starts clean and saves nothing, or two windows overwrite each other and a one-file window drags the last project’s tabs in. Wants an ephemeral switch (aShelloption, defaulting to today’s behaviour) thatWindowMemoryand the session keys honour. The editor’s side is in its owndocs/TODO.md. - Not ruled, recommended: split general settings (theme, font, keybindings: shared by every window, wanted by every app) from the session (per instance). Save the settings by taking an OS lock (FileChannel.tryLockon a sidecar.lock: a created-if-absent lockfile goes stale when a JVM dies, and locking the file itself blocks the rename on Windows), re-reading, applying only the keys this process changed, writing a uniquely named temp file and moving it into place. The lock is short and never held across a dialog. Other windows may pick a change up from aWatchServiceor on focus; optional. Shared settings want a home shared across applications, whereAppHometoday is per application (~/.{name}), e.g.~/.vexelray/.**Why after v1:** `Settings`' accessors and `save()` keep their signatures, the lock and merge are inside them, and the ephemeral switch is a new option with a default. The *`Window` seam* did not move it: `shell.memory()` is deleted and placement is per window inside the framework (`Windows`), so no application holds the store's session. **It moves to Blocks v1** if a shared home changes `AppHome.of`'s contract. -
A shipped application and a drivable one are two builds, and each app now copies the arrangement. Found 2026-10-05, removing the console window from the native executables. The automation facility (
ottermate) announces its port on stdout, so the first native builds were console-subsystem programs and a desktop shortcut flashed a console window; it also meant a shipped binary could open a driving socket. All three apps (text editor, vexplore, mainframe) now build two editions, each with the same shape: --Pnative-release:target/<name>.exe, linked/SUBSYSTEM:WINDOWSwith/ENTRY:mainCRTStartup(the image’s entry is the Cmain, notWinMain; the PE header reads Subsystem 2), and the automation dependencies set totestscope through anautomation.scopeproperty, so the generated wiring does not contain them. Checked: with--automation=0there is no listening socket for the process, and the stringsAutomationServerandAutomationStarterare in none of the release exes and in each debug one. --Pnative:target/<name>-debug.exe, console subsystem, automation present, driven withottermateand a screenshot taken for each app (the replies and byte counts were checked, not the pictures). - What it cost each app: a second source root per edition (src/edition-debug,src/edition-release) added by a build-helper property. Because the framework names starters in the@VexelAppannotation on the main class, vexplore and the editor each moved that annotation into a small class (VexploreApp,TextEditorApp) of which each edition has its own copy, differing only instarters; mainframe moved its socket code into a package-privateDrivingclass that is empty in the release edition. The generated wiring was renamed with it. - Not ruled, recommended upstream: a parent-pom or BOM profile pair carrying the linker flags and the scope switch (the flags are now written three times); reachability metadata in the backend jars (see the native-image metadata entry, which also removes the trap vexplore fell into, a missing-H:+SharedArenaSupport); and a way to name a starter that depends on the build, so an application does not need two copies of its@VexelAppclass. Substituting a source root from Maven is the awkward part of today’s arrangement. - Unverified: that no console window appears for an instant. Only childconhostprocesses were looked for, not the window list. Andexec:execwas not run after the change;mvn testpasses. Why after v1: the profiles are build files, not an API. It moves to Blocks v1 if the answer to a starter that depends on the build changes@VexelApp’s contract (a conditionalstarters), since that annotation is the first thing every application writes. -
The full mailbox dump on a FAIL overflow, beyond the report that now exists. A full FAIL mailbox halts the process on purpose (components.md, ruling 5). Done 2026-10-01, in atchung:
MailboxOverflow’s message now says who published the event that did not fit (thread name and the calling frame), when, and when the consumer last took a batch (or that it never has, and for how long since it subscribed), with no payload;Faultsalready writes that message to the log file and flushes before halting, so a framework change was not needed. The two readings it separates: drained a moment ago means the mailbox is too small, not drained for seconds or ever means the consumer is stuck. Not done, and parked deliberately: a separate file with every other mailbox’s depth, each lane’s busy state and every thread’s stack grouped by lane (the consumer’s stack is the “why”). If it is built: the overflow is detected on the publisher’s thread holding the mailbox lock, so the writer has a deadline (about 2 s) and the process halts regardless, other mailboxes are read with a try-lock, and the file is forced to disk first. The watchdog’s and the shutdown bound’s halts leave no record either and could share it. A per-mailbox overflow handler was designed and dropped; see ruling 5. A generated project needs the new atchung installed to see it. -
There is no one way to put a 3D viewport in a window, and every application re-derives one. The pieces exist and none of them is the pattern.
GuiApp.viewport(w, h)hands out a render target andNode.image(target)shows it; the engine hasRenderTechniqueandSampledTechniqueHost;vexelray-gui’sCLAUDE.mdsays the application “brings the technique”. What is missing is the statement of how, so each application chooses again, and they have chosen differently: -vexelray-designer’sViewportusestarget.pipelineForandrenderIntodirectly, owns its orbit camera (gui-plot’sCamera), resize policy (re-mint at a 1.35x growth threshold), input (drag, wheel through the bus) and axis swap, and overlays aSketch. -vexelray-sim-fluid’sDebugViewdoes the same by hand for a 2D field, with a fixed square target and its own storage buffer, and does not resize at all. -SdfRaymarchTechniqueandSampledTechniqueHostare the named route and cannot be used by either, because neither can bind an application’s storage buffer (SdfRaymarchTechniquebuilds its pipeline with no descriptor layouts). So an application that wants data in the march drops below the technique seam toSdfComposer.fragmentSpirv(...)with a hand-writtensdf(vec3), asConeFielddoes, and then has rebuilt the host.Found when asking how to render the 3D fluid (48^3 MLS-MPM) with the SDF technique: the answer was a survey of three repos rather than a call. What each application re-solves, and so what a pattern has to say once: who owns the target and its resize; how an application's data (a storage buffer, a texture, push constants) reaches a technique; the orbit camera and its input, including the y-up/z-up swap; overlays and depth against the rest of the GUI; and which thread and frame stage drives it (`renderInto` blocks on its own fence, so it is main-thread, `FrameStage.APP`). The fluid adds a case the pattern must not rule out: the data lives on a *different device* (`ParticleSimulation3` owns its own `Accelerator`), so it arrives by host readback each frame until the engine can share buffers, which belongs in `vexelray`. **What would close it:** one `Viewport3D` component, or a template in the project builder, that owns the target, resize, orbit and overlay, and takes a technique that can declare the buffers it binds; plus the technique seam growing that declaration. Where it lives is the open question: the lifecycle is this repo's (`Shell`, frame stage, disposer), the target and camera are `vexelray-gui`'s, and the binding is `vexelray`'s `RenderTechnique`. Decide which repo owns the contract before any of them is written. **Why after v1, not before:** a component that applications opt into breaks none that compiled without it. It moves to **Blocks v1** if the answer to "where does the binding declaration live" turns out to change `RenderTechnique`'s signature, since that is what an application's technique implements. -
-diagnostics, and moveFpsProbeinto it. It istext-editor-vexel-demo’s, about 250 lines, and generic apart from the one thing that makes it worth having: it deliberately pokes each wake path — a timeline post, a node mutated off the frame thread, a handler that changes nothing — to prove each still produces a frame. That is what--profileshould turn on, and it is the module the architecture doc already lists as the Actuator analogue. Until it exists,profileis a nameLaunchreserves and nothing in the framework honours — documented onFRAMEWORK_KEYSand in architecture.md, along with why the real fix is@ConditionalOnTypeand not a runtime check.**The module can land after v1; its name cannot wait.** A module name is Maven coordinates an application writes into its build, so it is chosen before the freeze even if the module is not. **The name is now taken upstream.** `../vexelray` ships a `vexelray-diagnostics` for a different thing — the channel a seam uses to say it silently dropped a capability (see **Next**). Two concepts, one name, one stack; this module wants a name of its own before it is written. **And most of it may already exist.** `atchung-probe` is described as *"the stack-wide profiling seam: off unless asked, free when off, and dependency-free so any layer can take it without taking the bus"*, with spans, counters, a resource ledger, a CSV correlation mode and a `CsvView` that hunts a run for stalls. `Pump` instruments itself with it and `Automation` already imports `Probe` and `Lane`. So `--profile` may be a few lines turning `Probe` on rather than a 250-line port, keeping from `FpsProbe` only the part that is genuinely its own — that it deliberately pokes each wake path to prove each still produces a frame. Worth checking before porting anything, and more so under the concurrency model, where *"why did we miss a frame"* becomes *"which component's mailbox"* and a probe already threaded through the bus is what answers it. -
A second automation socket, if a second application ever wants one.
Driverbinds one, for theGuithe framework built.vexelray-designerneeds two —treeon the first does not list the viewport andshoton it photographs the wrong window — and places the second itself atDriver.port() + 1, so it never parses the flag but cannot ask the framework for the socket. The shape would be aDriver.open(shell, gui, controls, offset). Not yet: one application is not a census, and the two things that make the designer’s second driver awkward (a named window’s controls arrive after the driver starts, and are replaced when it reopens) are facts about that window rather than about starting an application. Recorded so the second witness is recognised as one. -
An application that configures a non-default zoom range will disagree with the text editor’s other two windows.
FolderWindowandEditorWindowapplyAppearance.ZoomRange.DEFAULTrather than the running application’s, because both also run under MainFrame where there is noShellto ask. Correct today, since nothing configures a range; wrong the moment something does. The fix is a constructor parameter defaulting toDEFAULT, and it is not worth the churn on two library classes until a host wants one. -
shell.wake(gui::onWork)is redundant, and the framework should probably stop making the call.GuiApp.wireAllWakesalready doesgui.onWork(this::postWake)for every tree it presents, re-checked each iteration, and says why: “an application has more trees than it has main windows”, so left to the application it is “a line to repeat per window, correct on the window under test and missing on the one being used.” The framework’s single call is the weaker version of the same thing — one tree, once — and deleting it changes nothing, whichWakeSeamsTest’s falsification table records. Not obviously a deletion: the line documents that the framework knows the wake is owed. But it should say that rather than duplicate it, or a reader takes it for the thing that makes the wake happen. -
shell.deadline(() -> krono.kron().sleepTimeout().nanos())is not covered by anything, and the reason is interesting enough to keep. Deleting it fails no test: the clock’s wake already brings the loop back whenever the timeline has work, so liveness survives without it and what the deadline buys is parking until the next moment rather than being nudged to it. That is a latency and power property, which is exactly the classGuiApp.idleRefreshcalls “bounded, uniform, and survivable” — real, and invisible to an assertion about whether a frame arrived. It wants a timing assertion (a cue at a known moment fires within a few ms of it, and the loop woke once rather than a hundred times) oratchung-probe’s FRAME lane, which already instruments the wake/budget/frame chain. Parked until--profileis a real thing, since that is the same machinery. -
Three of the six dropped-capability reports are routed and not asserted.
InputBackend.open,InputBackend.perWindowandDriver.openare covered — the first two byDroppedCapabilitiesTest, which gets its absence free because no tactroller platform module is on-shell’s test classpath, and the third byDriverTest’s malformed port, which is the one way a socket fails to bind beforeshell.app()is reached. The other three need a fault that cannot be arranged from a test:InputBackend.attachwants a backend that opens and will not bind,ClipboardBackend.openwants a machine with no clipboard, andinstallMarkwantssetApplicationIconto throw. Each would need a seam taking the backend rather than opening it, which is more API than the assertion is worth today — recorded so the gap is a decision. -
PointerLock’s state machine is asserted; the line that installs it is not.PointerLockTestdrives the threshold, the focus rule, the modes and the teardown againstPointerLock.Device, which is the seam the previous entry turns down in general and which was worth it here because the thing behind it is a state machine rather than a one-line report. What is still taken on faith isgui.onPointerLock(this::want)— that the framework subscribes at all. Asserting it needs the dispatcher to fire the sink, which needs a press hit-tested against a laid-out tree, which isvexelray-gui-harnessrather than a unit test.WakeSeamsTestalready runs a realGuiApp, so the pattern exists; the missing part is a node that declaresdragLocksPointerand a synthesised press over it. -
Every animation starts with one or two frames 20 to 30 ms late. Found by the pacing measurements (what they found): on a loop whose frame gap is a steady 6.94 ms, the first ~60 ms of a 600 ms pulse contains one gap of 20 to 30 ms at about 30 ms in and often a second at about 60 ms, three or four frames dropped each, every time, the third pulse included, so it is not first-run warm-up. It is the one place the measurements show the loop failing the goal that nothing ever hiccups. Two stalls; one is explained. A (~29 ms at the click) is the 60 Hz ceiling parking the loop for 16.6 ms just as a
krono.rampwaits for its first tick, plus Windows 11 throttling the timer for a window that is not in front, which stretches 16 ms to 28. The second half is confirmed and fixed:vexelray-os-windows’sTimerResolutionmakes both calls,SetProcessInformation(ProcessPowerThrottling, IGNORE_TIMER_RESOLUTION)andtimeBeginPeriod(1), while any window exists (reference-counted per window), which took A from ~29 ms to ~18 ms.PacingMeasurementTestnow asserts it: no frame gap over 26 ms in the first 100 ms of a pulse. (timeBeginPeriod(1)alone does nothing, which is why it first looked ruled out.) A refusal by Windows is kept inTimerResolution.problem()and reported nowhere yet, because the os module has noDiagnosticsto say it with; the shell could ask. The rest of A is the ceiling, so it goes with themaxFrameRatedecision. B (~21 ms at 27 to 60 ms in) is explained, and it is not the GPU (what the stall probes found): the loop runs flat out for a few frames after a park, because every clock tick’s wake ends the ceiling’s park, and the compositor’s present queue only pushes back once it is 3 to 5 frames deep. The wait is that queue draining. The fix that works, and needs no platform feature, is a ceiling that holds: re-park with a timed sleep (notwaitEvents, which returns at once on an unread wake message), anchored at the frame start, set to the display’s refresh interval. Measured with 6.944 ms, every pulse after the first runs 5.4 to 8.5 ms a frame from the first frame. The interval now comes from the window (maxFrameRateToDisplay); what remains is a per-monitor read, and the Linux and macOS sources. (DwmFlushalso worked and is a flag, off:-Dvexelray.present.flush=true.) A0, new: in the first pulse only, the first tick takes 4 to 6 ms to reach the animation (kron.ramp’s first spork), which is most likely cold code and is not a thread-hop cost (each baton pass is 50 to 100 us). Ruled out for both: a JVM pause, the uncommittedGuiAppdraft, the wake path. A per-frame assertion belongs invexelray-gui-harnessonce it is understood, since a frame-gap ceiling is the regression test for smoothness. Not measured, and needs engine work: input to glass. The engine enables onlyVK_KHR_swapchain, soVK_KHR_present_waitorVK_GOOGLE_display_timingmeans enabling the extension and its feature chain inVulkanDeviceand adding the wait toWindowedPresenter, on a driver that may not offer either. -
A test for the second close gate. Done 2026-10-11, without a
GuiApp: the gate isWindow.onClosenow, which refuses a second on the same window whatever the phase, andWindowsTestcovers it headless (eachWindowHasAGateOfItsOwnAndOnlyOne); W1 drives a gate on a real window. -
The designer’s comment describes the mode the framework does not use (fix belongs in
vexelray-designer).Viewport.javasays “turning is a displacement, so the pointer is held for the gesture and warped back each frame” — warping isPointerLockMode.RECENTER, andPointerLockdefaults toRAWprecisely so that nothing warps and the cursor reappears where it vanished. The line was written when nothing carried the intent out at all, so it described an intention rather than an observation. Now that it does, the comment is the only place on the stack that still says the cursor moves.
Found by Vexplore
The first application built on the generated template by someone other than its authors: a file explorer
(../vexplore, with its own notes). Generating it, building it,
reaching a screenshot and driving it with ottermate all worked, and the layout came out close to the design on
the first build — so what follows is friction, not failure. Ordered by how early a newcomer meets it. None
of it would break an application compiled before it lands, which is why it is here and not above; the first
two are the ones a new project meets before it has written a line, and would be the first things to do if
onboarding is the goal.
-
There is no route from the README to a project. The README says what a project looks like and never how to get one.
new vexel-desktopis inmainframe, whosemainframe-distjar on disk fails withNoClassDefFoundError: dev/mainframe/gui/desktop/Desktop$Apps. What worked was a short Java program against-template’sCatalogue.bundled(),Answers,Scaffold.ofandBlueprint.Writing— what-acceptance’sSupport.generatealready does. Fix: amainin-template(or a script beside it) taking--in,--name,--group, and a paragraph in the README. Themainframeroute then becomes a second way to run the same thing rather than the only documented one. Done:Generatein-template(GenerateTest), and Starting a project in the README. -
The template’s
Modelhands every application a race.Model.onChangeisstate.onCommit(...), which delivers on the committing thread after the compare-and-set, and the template says every handler commits from a worker pool. Two handlers finishing together deliver as version 6 then 5, or concurrently, so aUi.show(Doc)written exactly as the template says can finish on a stale document, or run twice at once. Vexplore hit it as a rail with the wrong contents and rebuilt panels that lost their landmarks. ItsModel.onChangenow takes a lock and drops any document older than the last it delivered (tested with eight threads). Fix in the template’sModelat least; better an ordered variant on atchung’sState(onCommitLatest), since the shape is not specific to this framework. Also in atchung:State.commitcounts a function that returned the same value as a new version, so re-announcing an unchanged selection wakes every listener; acommitIfChangedwould remove a check every caller writes. Done: atchung’sStategainedonCommitLatestandcommitIfChanged(tested there), and the template’sModel.onChangeis one line overonCommitLatest, with an eight-thread test in the generatedModelTest. A generated project now needs an atchung installed with those two methods. -
A
Wiringkeeps its parts private, so a capture or a test cannot reach the model.<App>Wiringholds each part in a private field with no accessor. To put Vexplore in a known state for a screenshot it reads four system properties inStartup, applied after the first listing. A generated, test-visibleparts()(orWiring.get(Class)), returning what theRecipesbuilt, would letCaptureand a unit test drive the same objects a user does. Additive. Done: the generated wiring has a package-private accessor per part, named for its field (wiring.model(),wiring.ui()), typed and null before its phase;VexelApplication.tree(new XWiring(), args)leaves the caller holding the wiring. Notparts()orget(Class): a lookup by class would be the reflection-shaped route this repo avoids. -
Recipescan only say “after” by taking a parameter it does not use.Uineeds thePreviewer’s listener registered before it seeds the model, and the only way to say so isui(..., Previewer previewer, ...)with the parameter unread. It works, and it is the right instinct that order comes from dependency — but a part that must come after another for timing and not for data has no name for that.@Provides(after = ...)would say it in the place a reader looks. Additive. -
The template ships
Capture.javaand the docs retire it.vexelray-gui/docs/reference/automation-cli.md§2 and §7 call per-application--capture“correct about the chrome and silently wrong about the content” and listmainframe-template’s copy for removal;-template’sfiles/Capture.javais still generated and the README teaches--capture out.png. Vexplore has no viewport so it was right, and it was the fastest way to see a layout, butottermate’sshot(the replacement) worked immediately once used. Decide: deleteCapture.javaand--capturefrom the template and teachottermate --launch ... shot, or keep it and say when it lies. A POSIX-style path (/c/Users/..., from Git Bash) passed to--capturealso ends in Maven’s bareExit value: 1with no message from the application. Done, 2026-09-30: deleted. Screenshots areottermate’s only, now that it can set what a picture is of (--zoom,--dpi,--sizein px or em,--window, andwindows/windowfor several windows). The template’s README has a Taking a screenshot section;GeneratedProjectTestdrives the new verbs against a real window and checks the PNG is the size asked for. -
The generated
.gitignoreignores every PNG.*.pngis meant for captures and also hides a project’sdocs/screenshots — silently, and this project began from a folder of them. Ignorecapture*.pngandtarget/instead, or add!docs/**/*.png. Done: the template ignorescapture*.pngandtarget/. -
Everything the template writes is one package of package-private classes. Right for a counter and wrong for anything with a file system layer, so Vexplore’s
files/andsuggest/are sub-packages with public types, testable with no window. Worth a paragraph in the README, How it is put together, on where a real application’s non-GUI code goes and that it should be testable without aGui; and possibly a second template that starts there. Done (the paragraph): in the README’s Starting a project. Done (the template):vexel-desktopnow writes a text editor, whoseTextFileandMarkdownare public classes in atextsub-package, tested without aGui. -
Widgets that a second application wrote in an afternoon.
Button(with a toggle form),Breadcrumb,StatusBarandSplitPanewere added tovexelray-gui-widgetwhile building Vexplore, with tests, andListView.marked/looks,Table.headersandTable.onSortextended existing ones;Ui.buttonanddocs/framework-notes.mdFN-0 in the template describe the world before them. Do: delete FN-0 from the template’sframework-notes.mdand replace the template’s hand-rolledUi.buttonwithButton, so a new project starts on the component. The-templateacceptance run will show whether the template still builds. Done:UiusesButton; FN-0 is gone. The acceptance run builds and drives it.
Second round (actions and modifier keys) — FN-12 to FN-15. Nothing here
blocks v1, and one thing worked better than expected: the processor’s compile errors (a record behind @Provides;
GuiApp injected into something that is not @MainThread) were each right and each named the fix.
-
A part that needs the device later has no direct way to say so. Textures need the device, which exists only from
WINDOW, and a part takingGuiAppis itself@MainThreadand pulls everything that takes it into the window’s phase, so a headless capture can no longer build the tree. The way through is the README’s clipboard shape: a tree-phase object, plus a@MainThread @Provides AutoCloseablethat binds it to the device and un-binds on close (Vexplore’s and Pix’swindowBinding). It is right and roundabout; a provider that runs when the window exists and returns nothing the container needs to hold would say it in one line. Narrower than it was: the folder dialog it was found with (Vexplore’sChooser) is the framework’sDialogssince 2026-10-11, which hold a question until there is a window, so only the device is left late-bound. -
settledoes not wait for the handler and offload lanes. The rest of this entry landed invexelray-gui-automation: chords inkey(2026-10-12), andawait <landmark>with no text,keydownandkeyup(2026-10-18, protocol 1.2; held keys are released atquit). What is left issettlealso waiting for the lanes to drain (see thesettleentry under Upstream), so a click-settle-shot scene photographs the answer rather than the state before it.Support.Driver.firstFramestill pollsfind, and could use the no-textawaitonce the witnesses run against an install that has it.
Found by the rebuilt editor and calculator
Vex (the text editor, its notes) and the calculator
(its notes) were generated from the template in October 2026
and rebuilt fresh. What took their requirements unmodified is the larger half (the generator, phase inference, one
Settings, window memory, the title bar, the close gate’s place, Launch.rest(), the dialogs, the logging), and an
editor with tabs, a navigator, a close gate and a remembered session has no line of application edge. What they
found that touches a frozen surface is filed above: the Window seam’s three witnesses, TREE, the look, chords in
key, and a third trip over one bus per Gui. Already closed: the editor’s icon (FN-2, now @VexelApp(icon)), the
calculator’s crash on closing a second window (FN-4, see architecture.md, the frame loop is the lifecycle),
Popout’s shared bus (calculator FN-2), the stall behind a file dialog on Windows (Vex FN-7’s update), and a
generated project with no motion (FN-12: the template builds Motion). The rest:
-
A recipe run for its effect has no shape. Arming the close gate and restoring a session produce nothing, and a
@Providesreturningvoidis an error (“provides nothing”), as is one returning a record. Vex returns an empty marker class (Recipes.Restored), which the wiring stores and exposes; Pix’s window binding (above, a part that needs the device later) returns anAutoCloseableand had to merge two late-bound parts into one provider because two would be ambiguous. Three applications, one missing thing: a recipe the wiring runs for effect in its inferred phase. Acceptingvoidlater removes an error, so it is additive. The messages no longer send such a recipe to an interface (2026-10-18):void, an empty record and the record rule each name the marker class and theAutoCloseable. -
onCommitLatestmakes a committer wait on another thread’s listener (atchung, Vex FN-4). Commit while holding a lock the listener takes and it deadlocks, with nothing thrown; and the order it keeps is among its own deliveries only, so a direct call racing a delivery lost Vex’s last opened file from the saved session. The template’sModelnow warns about the lock. Delivering through a serial executor with the same version drop would mean a committer never waits. -
A change made off the frame thread is sometimes not drawn until something else draws (
vexelray-gui-core, Vex FN-5 and FN-13). Syntax colours set from a worker appear on the next caret blink, and a tab change once stayed undrawn until the pointer moved;requestFrame()aftersetSpansdid not help. Suspected: vexelray-gui8a74b41(a frame is drawn when something changed), not bisected. Not a framework surface, butShellhands outGui, so its draw-on-change contract bounds v1. Read 2026-10-18, and no lost draw found: publish, wake, the draw flag and the park were each traced, andDrawOnChangeTest(vexelray-gui4774e69) drives 500 bursts of worker-published spans through a parked loop, and fails if the wake is cleared late. The likelier cause issettlereturning before the highlighter’sgui.asynchas published: async work is not counted inOwed, andshotalways draws fresh, so an uncoloured shot means unpublished spans. That makes it thesettleentry below, not a draw defect. Not checked: the editor itself by eye (a caret blink seen by a person is not a shot). -
Widget gaps, each found once (
vexelray-gui-widget, Vex FN-8, 9, 11, 13, 17 to 20).TextField.onChangeis one slot and fires on span-only changes;Tabs’ own Close cannot be stopped for unsaved work (the bar now scrolls when the tabs do not fit);History.mark()marks now, not a version;SplitPanecannot collapse a pane; the dialogs’ title bar offers minimise and maximise; a late publication into a closedGuithrows; a node does not clip its own glyphs, and there is no ellipsis (wordWrap(false)on a label now holds); no current-line or active-gutter role;TreeViewhas one item per row; a sliding tab indicator is sixty lines every application with tabs writes. Additive; the units half of FN-20 is units.md.
Upstream
Cannot be fixed from this repo. The one that blocks v1 is under Blocks v1; these do not.
-
On a machine whose only Vulkan is dzn (Vulkan over D3D12), the window hangs open and the first frame fails (
vexelray-present-dxgianddxgi-bridgefor the frame;vexelray-gui-coreGuiAppandvexelray-os-windowsWin32Windowfor the hang). Found 2026-10-16 from a report on an older computer: dark window, Not Responding. Reproduced here with Microsoft’s Compatibility Pack, dzn forced onto WARP and the process CPU-capped; the steps, scripts and transcripts arevexelray-gui/docs/guides/no-native-vulkan.md. Two separate faults. The frame: dzn makes the image the presenter shares with DXGI a simultaneous-access texture, then emits render-target (or, inGENERAL, direct-queue-common) barrier layouts on it, which D3D12 refuses (debug layer error #1334), sovkEndCommandBufferreturns -1 atDxgiPresenter.java:307. The same happens on dzn over a real GPU, and dzn draws offscreen correctly (EffectsRenderTestpasses on it), so only the shared image is the problem. The hang: the main window is shown when it is made, and nothing pumps its messages again untilRUN; device selection, the device, DXGI, the atlases andATTACHall run in between, which is about a second here and more than 5 on a slow enough machine.Win32Window’s comment at thecreateWindowExWcall still says the window is created hidden until the first frame, which the code below it no longer does. Not a v1 blocker: no application code changes for either. Ask: a shared resource dzn cannot mistranslate (a buffer has no layouts: draw into a private image, copy it to an exported buffer, and have D3D12 copy that to the back buffer), at least when the driver is dzn; and pump the main window between the bring-up stages, or keep it hidden until the first frame, which needs deciding against no visual discontinuity. Report the barrier bug to Mesa as well, since machines will carry whatever dzn they have. -
DXGI is the default presenter on Windows, and
dxgi-bridgehad no native-image metadata — fixed upstream. VexelRay on Windows presents through DXGI and only DXGI:vexelray-gui-core’s Windows profile carriesvexelray-present-dxgi, andGuiAppwill not start there without it (the swapchain is too fragile to fall back to). In a native image every D3D12/DXGI downcall and COM vtable call has to be registered under"foreign"(vexelray/docs/native-bindings.md§6), and neitherdxgi-bridgenorvexelray-present-dxgiregistered any. Without them the calculator’s native binary died at startup withMissingForeignRegistrationError. Both now shipreachability-metadata.json. They are written from everyFunctionDescriptorin the source, not only from a trace, because COM methods bind lazily andDxgiSmokeTestreaches only some of them; the trace was used as a cross-check. -
With two windows and none focused, the loop can park for good with a wake outstanding (
vexelray-gui-coreGuiApp’s loop) — fixed upstream (vexelray-gui809c584, withTwoWindowsWakeTest). Found by W1; the trace and the reading are in what W1 found. A wake that lands during a frame sets the tree’s frame owed flag and posts a message. When more than one window pumps the thread’s queue in that iteration, the message can be consumed before the park, and the loop then parksforeverbecause nothing is focused, while every later wake is suppressed because the flag is still set. A component’s result arriving into an unfocused multi-window application is never drawn. Ask: do not park whilewakePostedis set (it is already recorded, and read only for the frame ceiling), beside the existing refusal to park on a queued task. That is what landed. -
On macOS, a modal dialog on the GUI thread is reported as a stall (
vexelray-gui-coreGuiApp.reportStall). The screenshot button opens a native save dialog (vexelray-gui-nfd’sSaveScreenshot, throughFileDialog.saveAsync). On Windows that runs on the module’s own dialog thread and the window keeps drawing, so nothing is reported. On macOS AppKit requires the window’s thread, the dialog blocks it, and the first one in a process takes more than 250 ms. SoGuiAppwarns once that a task stalled the window, and advises the offload lane, where the dialog cannot run. Ask: a way to post work that is modal by design (GuiApp.postModal, say), which the stall check leaves alone, so the warning stays reliable for work that should not block. -
The layout has no wrapping row, and the palette has one accent (
vexelray-gui-core). Found by Vexplore (FN-5, FN-6). Chips that flow onto the next line are counted two to a row by hand, which is right at one zoom and wrong at another; the design wants teal for selected and amber for the application is speaking, and gets the second by borrowingPalette.action, whose documented meaning is the fill of a filled control.Rolebeing an open functional interface made the application-level wash colours easy; a documented pattern or a third anchor would make it a decision. Also, the resize cursor (vexelray-gui/docs/plans/todo.md§4.2) now has two consumers,Table’s grip andSplitPane’s divider, and neither can say what it is. -
A texture can be made and not safely let go (
vexelray-gui-coreGuiApp) — fixed upstream. Found planning Vexplore’s image preview, which isImagelib.decode→GuiApp.texture→Node.image, a new texture per selection.texture’s Javadoc says content that changes “closes the old one after a frame that no longer names it”, and that puts the duty on the one party that cannot see it: the device and the present fences are private on purpose, so an application can only guess when no frame in flight still samples a texture. Too early is a use-after-free on the GPU; never is a leak until exit, which is all “closed with the application” offers. Right for icons loaded at startup, wrong for anything that browses. Ask:GuiApp.release(texture), which queues the texture and closes it once a presented frame no longer draws it (or a retire queue keyed on the frame counter, which comes to the same thing). It is the invariant an application cannot be trusted to re-derive, so it is the framework’s.`GuiApp.release(texture)` is what it got, and the split it draws is the one the ask implies: the tree is the application's half (release after the last node showing it has another image), the GPU is the framework's. A released texture is held one whole loop iteration, so an edit posted just before the call has been drained by every window, then closed after one `waitIdle` at the top of the next — which, with one frame in flight, is what the next frame's fence wait would have cost. A frame counter alone would not have done: windows draw only on change, so an idle window's last submit is never fenced by a later one. A texture some window's tree still names is not closed but kept and warned about once, since a dangling descriptor is worse than a leak; one the application did not make is refused at the call. `TextureReleaseTest` in `vexelray-gui-harness` drives both orders on a real device. `viewport`'s replaced targets have the same shape and were left alone, having no consumer that browses. **The cache on top stays in the application for now.** The policy is LRU (a browser goes back to what it saw recently), bounded by bytes, keyed by what the application knows (Vexplore: path, size, mtime, and the box for an SVG), with eviction calling `release`. Decoded `Frames` and GPU textures want different budgets, which is one more reason not to fold them into one framework buffer. If a second consumer appears (Vexplore's row thumbnails count only as a second use, not a second application), an LRU texture cache over `release` is the candidate to extract. -
Modalsnever receives the application’s theme (vexelray-gui-widget) — fixed upstream and taken here. It built its ownnew Gui(), which defaults toTheme.DARK, so every dialog the framework installed drew dark whateverAppearance.theme()said — against a class whose own Javadoc promises a dialog “drawn with the same chrome as the rest of the application”. Invisible intext-editor-vexel-demo, whose theme isTheme.DARK; visible incalculator-vexel-demo.The signature this note predicted is the signature it got: `Modals.install(app, Consumer<Gui>)`, applied to the dialogs' tree *before* it is built, because a role resolves at the moment a widget writes a colour. `VexelApplication` now calls it with `appearance::applyTo` — the same seam the designer's viewport window and the editor's other two use, and the case `Appearance.applyTo`'s own Javadoc was written about. Two things worth knowing, both decided upstream. The one-argument `install` stayed, documented as the library default look for an application with no decision to pass on, so the wrong call is now a choice rather than an accident. And the dialogs re-derive their page and message colour per dialog, so an application that changes its look at runtime gets a correct *next* dialog — the buttons were already being rebuilt, and those two were the only parts that would have stayed stale. Guarded by `DialogsWearTheApplicationsLookTest` in `vexelray-gui-harness`, which asserts the seam was handed the tree the dialogs are actually built on rather than merely a `Gui`. Writing it needed a fix of its own (`vexelray-gui` §6.11): the harness made its first window on the calling thread and the rest on the loop thread, and a dialog is modal, which was enough to leave no thread able to tear them all down. -
The frame ceiling holds and follows the display; what is left is its input latency and its reach (
vexelray-gui-coreGuiApp,VexelApplication.MAX_FRAME_NANOS). The entry this replaces asked whether a ceiling was the intent; it is, and it is built. The ceiling was a timed park that any wake ended, and the krono clock wakes the loop every tick, so nothing was held to it: after a park the loop drew frames about 1 ms apart until the present queue pushed back (what the stall probes found), and an animation crawled and then lurched at its start. It now sleeps out the rest of the gap after a wake (LockSupport.parkNanos, anchored at the frame’s start), at one display refresh read from the window (maxFrameRateToDisplay, 60 Hz where the window cannot say). Open, in order of weight: - Input that lands in that sleep waits for it, at most one refresh interval (6.9 ms at 144 Hz, 16.7 at 60), and only when a wake and not input ended the park. A Win32 event waited on beside the message queue, so a wake never leaves a message behind, would let the re-park bewaitEventsagain and restore costs nothing in input latency. Invexelray-os-windows. Not measured: how often a click lands in the sleep. - The rate is the compositor’s, not the window’s monitor’s. Right with one display or displays of one rate; a per-monitor read (MonitorFromWindowand the display’s current mode) is exact. Linux and macOS report no interval and keep 60 Hz. - Timed parks are millisecond-grained and not locked to the vblank, so gaps scatter by about 1.5 ms around the interval (5.4 to 8.5 ms at 144 Hz). Bounded and invisible beside a 20 ms lurch; it is not vsync, and stamping each frame with when it will be shown is the exact answer if it is ever seen. - A test.PacingMeasurementTestasserts no gap over 26 ms in the first 100 ms of a pulse. The old behaviour (21 ms) passes that too, so nothing guards the ceiling. A ceiling near 12 ms would, and wants the first pulse’s cold-start gap (13 to 17 ms) excluded, or warmed. -
settlesaysokfor an application that never goes quiet (vexelray-gui-automation, and this repo’sDriverif it grows a verb). The long-running witness’s metronome ticks to itself, andsettleansweredokbetween two ticks, as its javadoc says it may: it is exact about the frame loop and the clock and blind to application work in flight. Nothing in the socket can say this application does not go quiet, so a script that settles before a photograph of an animating window photographs it mid-change. Either the framework offers a way for an application to declare it is busy, whichsettlewaits on, or the documentation says loudly that a photograph wants a landmark to wait on.**A second witness, and a different cause: an application that goes quiet but is not done.** Vexplore lists a folder on `Gui.offload()` and rebuilds a panel on a handler thread, so `settle` returns while the answer is still on its way and a scripted click-settle-shot photographed the state *before* the click, every time. The loop was quiet and the application was not. The workaround is to write one status slot last and `await <landmark> <text>` on it. `Lanes` already knows how deep the offload and handler queues are, so `settle` could wait for both to be empty as well; and `await` cannot wait for a landmark to *exist* without text, which is the form a script wants for "the dialog appeared". -
The pointer lock is asked for on the press, not on a drag (
vexelray-gui-core).InputDispatchercallsrequestPointerLock(dragLocks.contains(...))inside itsButtonPressedcase, besidefireDrag(START)— so the intent arrives before the pointer has moved at all, and a plain click on a viewport asks for the cursor to be hidden and then released a few frames later. Carried out literally, that is a flicker on every click on the designer’s canvas.Worked around downstream rather than fixed: `PointerLock` holds the intent until the pointer has travelled `DEFAULT_THRESHOLD_PX`, so a press that releases without moving never hides anything. That is the right place for *a* threshold — travel is a fact about the device, which is the framework's side of the seam — but it is not the right place for *this* one. The dispatcher already owns a recogniser that draws exactly this distinction, in `DragGesture`'s own words: *"distance decides whether it was a drag"*. A lock requested on the promoted drag rather than on the press would need no threshold here at all, and would fix it for every consumer of `vexelray-gui` rather than for applications that happen to run under this framework.