Brodgar client

The tick and draw traversals: how a frame reaches a widget

Covers: the two recursion seams, what an override of draw does and does not carry to its children, where the screen GOut is built, and what a frame allocates. Split out of widgets.md, which keeps the tree and the create/place/destroy seams.

The two recursion seams

What Where
Tick root UI.tick → dispatch(root, new Widget.TickEvent(delta)) → UI.dispatch → ev.dispatch(to)
Tick recursion point (the ONE seam) Widget.Event.dispatch — handle(this) then propagate(w); TickEvent.dispatch overrides it to carry visible
Tick descent / leaf call TickEvent.propagation walks from.child→next; shandle calls w.tick(ev)
Draw root UI.draw → root.draw(g) (direct — not through a dispatch), then afterdraws
Draw recursion point (the ONE seam) Widget.draw(GOut,boolean) child loop — xlate+reclip(l), CPUProfile.begin(wdg), Fonts.frame(wdg), wdg.draw(g2)
gtick (render hand-off) UI.gtick → GTickEvent — a separate pass, inside the same utick phase
Frame phases around them UILoop.Frame.tick (dsp/stick/utick), display (draw)
Per-widget cost probe (fork) // addon: Widget.prof + profadd (Widget), the two seams above, and the root bracket in UI.draw
Per-widget decoration seam (fork) // addon: Widget.addonovs (null on a widget carrying none) + the call in the child loop placed after wdg.draw(g2) and outside Fonts.frame(wdg), with the root’s twin in UI.draw after root.draw(g). The g2 it is handed is already translated and clipped to that child’s box

Gotchas. Draw iterates child→next (bottom-first), hit-testing lchild→prev (topmost-first) — opposite, both correct. draw skips !visible children; tick does not (TickEvent.visible goes false for the subtree). The root is reached by neither seam (its “parent” is UI), so anything wrapped per widget misses it unless UI.draw/UI.dispatch is handled separately. TickEvent.shandle tolerates a widget removing itself mid-tick — hence the cached next — and a widget may equally rebuild its own child list in its tick: handle (→shandle, the widget itself) runs before propagation, which then re-reads from.child fresh. That is what makes Window.tick a legal place to chdeco (035).

Monitor gotcha — a tick is a callback with that tree’s monitor ALREADY HELD, and so is a draw. UILoop.Frame.tick takes synchronized(ui) around ui.tick(), and UILoop.display takes it around ui.draw(g) — so every Widget.tick and every Widget.draw in the tree runs inside it. A widget added to a tree purely to get a per-frame callback is therefore a per-frame callback that starts out holding a lock, which is invisible at the call site: nothing in tick(double)’s signature says so, and the traversal that reaches it is three classes away. Anything such a callback does that takes a second UI monitor — and there is more than one UI in this client (multi-session.md) — nests two, against a Loader thread that holds them the other way round in Widget.add. Per-frame work that must reach another tree does not belong on a widget at all; call it from Frame.tick, outside both blocks, where it holds none.

super.draw gotcha — a subclass decides whether its children are painted at all. Widget.draw(GOut) is draw(g, true), which is the child loop and nothing else. So a class that overrides draw(GOut) without calling super.draw(g) paints itself and then paints no child, and around forty of the overrides in src/haven do exactly that — Buff, IMeter, MenuGrid, FlowerMenu and ChatUI among them. Nothing else about such a widget shows it: its children are added, ticked, hit-tested and listed by children() as usual, and only the picture is missing. Adding a child is therefore not a general way to draw over an arbitrary widget. WItem is the exception in this tree: an item icon is the first widget anyone wants to put something on top of, so it chains (// addon:, last, over the sprite and its info overlays) and a child built into an icon is drawn.

And WItem is the only icon that chains — the other three do not. Makewindow.SpecWidget.draw is drawbg + drawicon, SListWidget.IconText.draw (which BAttrWnd.Constipations.ItemIcon extends) is drawicon + drawtext, and a .res widget drawing its own ItemSpec answers to nobody here: none of them calls super.draw, so a child added to one is invisible. The decoration seam in the child loop above is unaffected, because it is the parent that paints it after wdg.draw(g2) returns — so an overlay lands on every one of those widgets, and only a child widget does not.

The style frame is per widget and nests. Fonts.frame(wdg) is opened around wdg.draw(g2) in the loop above, so it covers that widget’s own text and its whole subtree while its siblings resolve under whatever the level above them carries — and Fonts.gen() therefore differs between draw sites within one frame, which is what makes it a key component rather than a generation to compare against. The root’s own frame is UI.draw’s, since the loop never reaches the widget it starts from.

The 2D draw target

What Where
Where the screen GOut is built UILoop.display: basestate() (a BufPipe .prepped with wnd.fbstate() — the framebuffer state the toolkit window itself supplies) .prep blend + States.Viewport + Ortho2D + FrameInfo, buf.clear(...), then new GOut(buf, base, wnd.sz()) → ui.draw(g) under synchronized(ui). The 3D scene is inside that traversal (the MapView is a widget), so this ONE Render carries the whole frame in order — see world-3d.md for drawing a subtree into a texture instead
Clipping a child’s target GOut.reclip(c, sz) (strict) / reclipl (loose) — the child loop picks by its own strict flag, and a strict box is what cuts a child’s drawing off at its own edge
A parent’s chcolor reaches no child GOut(GOut), the copy every reclip is built on, copies def2d and never cur2d: a child’s GOut starts from the tree’s default 2D state, so a chcolor set on the parent’s GOut before super.draw(g) tints what the parent itself draws and nothing below it. chcolor() is cur2d.put(BaseColor.slot, …) — a reset to the default’s colour, not to the previous one — and defstate() copies the whole default back. Fork: GOut.tinted(Color) (// addon:) is a sub-view whose default carries a BaseColor, so the tint descends through every reclip beneath it; under it chcolor() comes back to the tint and chcolor(c) is c times it. BaseColor is a multiply in the fragment shader (u_color), one slot, so two tints never stack — the later chcolor replaces the earlier
2D drawing itself GOut — image/text/rect/line/prect/chcolor; the overloads and the 9-slice contract are in widgets.md
A PView draws into whatever GOut it is handed PView.draw renders its scene into its OWN colour target (fragcol, a texture the id_fb basic state sizes from its FrameConfig) and then resolve(g) blits that texture into the given GOut (resolveout(g, …).image(…)), post-processors and the resampler applied on the way. So an Avaview, a MapView or any other PView composes correctly into an offscreen 2D target as it does into the screen’s — the mechanism a widget standing in the 3D world and a mirror of a widget both rest on. PView.frame() composes a Glob.FrameInfo from ui.sess, so the widget must still stand in a session’s tree. ⚠️ A PView scene renders ONCE per frame: PView.maindraw → GLDrawList.draw → settingbuf.get(0) (GLDoubleBuffer) waits for the previous submission to have run on the GL thread, so a second draw of the same view inside one frame’s Render blocks the UI thread for ever — holding synchronized(ui), which stops the whole client with no error. Fork: PView.draw (// addon:) keeps the Render it last rendered into (lastout, one per frame from UILoop) and a second draw into it only resolves
gtick reaches a PView through the widget tree alone UI.gtick(Render) dispatches a Widget.GTickEvent over the tree, and PView.gtick runs its TickList (a Composited’s equipment sprites, a Skeleton.Debug); UILoop.Frame.tick sends it to the drawn session and to the addon layer, and Sessions.gtick sends a background member its Glob.gtick only — a PView in a session nobody is looking at is ticked (u.tick(), so Avaview.tick/Composited.tick pose it) but never gticked unless something dispatches the event to it (ui.dispatch(widget, new Widget.GTickEvent(out)))
Drawing a tree to an image, off screen haven.rs.DrawBuffer(Acephal.instance().env(), sz) — a real GL target on the headless renderer (JOGLOffscreen, which needs no java.awt.headless and no window) whose draw(g -> …) runs the very same Widget.draw pass; g.getimage(img -> …) hands the pixels back. Window.main is the one-widget example. It is the only check that sees what the pass does to a widget’s geometry — Frame.xlate, a Label restyling on its first draw — where a read of child.c and child.sz does not: a layout question is settled there, not on a picture composed by hand from the rasters

Per-frame allocation (garbage, not time)