---
status: in-force
summary: FNS_OverlayRegistry, the twelfth registry -- every tool hands over one layer TOP, the registry composites them, shows the result above the nodes through one utility annotation, and tells each layer where it draws; tools can also pin small interactive panels (widgets) to a corner. CookBar is the first client of both.
since: 2026-10-11
verified: 2026-10-11, pilot layers and CookBar live: composite, stacking by Menuorder, teardown on Displayed off and on deletion, pre-save removal, clone-master skip, bar positions equal NetworkToPixel; the viewed network's display flags, annotation alphas and children unchanged. Widgets: cold boot, corner stacking measured (margin 30, gap 20: second widget 30 + 170 + 20 points down), margin change and package save, Top List rows, tabs and click framing. Non-Commercial (2026-10-15, under app.addNonCommercialLimit): View pars equal the drawn size, 1280x602. Not verified: macOS.
skill: fns-registry
---
The client renders, the registry composites. A tool tells the registry "here is my output TOP, composite it with the rest", and in return the registry gives it everything it needs to render: where the view sits in the network, the scale, the resolution and which network it is. What a layer draws, and how, is entirely the client's business (owner, 2026-10-11).
The registry owns the rest, once for everyone:
- following the network editor you work in (the current pane, else the network editor followed last while another pane type is current);
- the composite: one Composite TOP (
over), the shown layers top first (higherMenuorderon top, verified), ending innull_overlay; - showing it: ONE utility Annotate COMP in the viewed network,
layerzone = abovenodes, transparent,enableoff, fitted over the view plus a 15% margin every frame the view moves (TDCollab's partner overlay,CollabPaneSyncHandler._drawOverlay, RemoteTD, generalised).
The global's View page, written only when the view changes, so anything bound to it recooks only then:
| Par | Means |
|---|---|
Viewx0 Viewy0 Viewx1 Viewy1 |
the rectangle the overlay covers, network units |
Resolutionw Resolutionh |
the resolution to render at, pixels (monitor DPI, capped at 4096; longest side 1280 under a Non-Commercial licence, so this is also the size TD draws) |
Unitsperpixel |
network units per layer pixel |
Pixelsperpoint |
layer pixels per screen point (keeps a line one width on screen at any zoom) |
Zoom |
the network editor's zoom |
Network |
the network being viewed, empty when there is none |
View() returns the same as a dict (x0 y0 x1 y1 width height unitsPerPixel pixelsPerPoint zoom network shown), and NetworkToPixel(x, y)
turns a network position into layer pixels (origin bottom left).
Parameters, not a tdu.Dependency. A dependency lives on the extension
instance and is rebuilt on every reinit, leaving every subscribed client
expression stale; parameters live on the COMP and survive it, and an
expression reading one is already a dependency. The registry polls the pane
each frame (TD has no pan/zoom callback; CookBar did the same) only while a
layer is shown.
Stamp a host (op.FNS_OVERLAYREGISTRY master's StampHost, then
Promotepars on) into the tool:
| Host par | Means |
|---|---|
Layer |
the layer TOP (a TOP par; Comp stays the base's COMP par, the tool) |
Menuorder |
stacking order, higher on top |
Displayed |
the user's show switch, a constant on the tool's Registry page (Ovdisplayed); the Overlay tab in FNS_Hub writes it |
Layeractive |
the tool's own half, an expression on its switch (Active by default), kept on the host; the layer draws only with both (see "Two flags per layer") |
A tool sets its own depth, by what it draws (owner, 2026-10-11). Stacking
only matters where layers cover the same pixels, and there the right order
is semantic (a cursor above an outline, an outline above a fill), which the
tool knows. Menuorder falls in a band:
| Band | For |
|---|---|
| 0-99 | fills and heat maps: broad, translucent |
| 100-199 | per-operator marks: CookBar's bars (100), outlines |
| 200-299 | labels and text |
| 300+ | cursors and other transient pointers |
Within a band pick a slot; a tie is broken by canonical name. The user
overrides per tool with Layer Order (Ovmenuorder) on the tool's
Registry page, which roams through the config file like every other surface
order, and the shipped value comes from packaging/surface_defaults.json. A
Hub tab to rearrange layers (as the toolbar and navbar have) waits until
there are about three clients; ConfiguratorBase makes it cheap then.
Render the layer at the View's resolution over the View's rectangle, premultiplied, transparent where it draws nothing, keeping the node tiles clear (it sits above the nodes).
- Idle is free. No shown layer: no annotation, no layer selects, the
frame driver (
execute_frame) off. The composite always has two inputs (the clear layer fills the second slot): a Composite TOP with one source errors "Not enough sources specified". - A deleted tool cleans up. TD calls no
onDestroyTDfor a host dying inside its parent's subtree, so every sync and the 15-frame heal tick drop entries whose layer no longer exists, and the frame driver turns itself off when nothing is shown. - Saves never hold it.
execute_save(projectpresave) removes the annotation; the next frame draws it again.pre_release_commonstrips any tagged annotation from artifacts, and the global sweeps the whole toolkit once at boot. - Never in a clone master. A child added to a COMP others clone
replicates into every clone: drawn in the registry master's own network,
the annotation appeared, path broken, in each host. Networks found by
_isClonedByOthersget no overlay; the answer is cached per network.
A package, not core (owner, 2026-10-11): tools that draw overlays require it,
through requires derived from the host (build_manifest.REGISTRY_OWNER),
and the installer resolves that like any dependency. This is the one
registry outside CORE; CREATING.md's "tools depend only on core" names
the norm, this is the exception. The shipped layout of a layer
(Ovmenuorder) comes from packaging/surface_defaults.json like every other
surface (docs/ShippedSurfaceDefaults.md).
glsl_cook_bar renders into null_cook_bar_layer at the View's resolution;
its zoom uniform is 1 / Unitsperpixel; the shader outputs premultiplied
bars with their fade as alpha and transparency elsewhere. execute_op_info
measures operators in layer pixels from View(). Gone: the Select TOP and
suicide DAT it planted in the viewed network, the display-flag switching, the
annotation dimming, the pane tracking (execute_pane_info, the window-size
tscript) and the save hooks (execute_save, putBack/reapply).
retireBackdrop removes, once at init, anything the backdrop method left in
a project. Annotationalpha is now unused (left in place for now). The Top
List is a widget (see "CookBar's Top List" below).
Interactive panels cannot go through the composite: it is one TOP, and an OP
viewer only passes clicks to the operator it shows. Annotations can show a
panel interactively (Annotate COMP docs: OP Viewer Interactive,
Opviewerinteractive, with Enable Interaction, enable, on), so a
widget is its OWN small utility annotation showing the tool's panel COMP.
A throwaway spike (a slider and a button pinned to the view's top-right
corner at a constant 220x64 points) measured:
| Question | Result |
|---|---|
| Interaction | works: the owner dragged the slider (0.414 to 0.384) and pressed the button (2 presses counted) |
| Clicks through to the network | no: the widget's whole rectangle takes clicks, so widgets must stay small |
| Zoom and pan | stays pinned at the same on-screen size (size and offset divided by zoom, refitted when the view moves) |
| The annotation's own resize handles | usable by the user; the next view change re-pinned it. The spike compared against its last written rectangle; a real version compares against the live geometry and re-pins at once |
| Cost | none measurable (12.5 ms a frame against a 13.3 ms baseline) |
| Placement | the top-right corner sat under TD's floating parameter window: widgets need anchor options |
Two traps found:
op()never finds a utility operator, which is what utility is for. The spike looked its annotation up by path, missed it, and made a new one every frame: 163 in a few seconds and 21 fps before it was stopped. Keep the OP itself (storage holds operator references), and guard against more than one.- A fresh annotation carries the provisional-name Component warning until
its
annotation/backis cooked once more (TDCollab's heal, needed here too).
A tool registers a panel COMP and a corner; the registry owns everything else: the annotation, the pinning, the layout, the pre-save removal.
reg = getattr(op, 'FNS_OVERLAYREGISTRY', None)
if reg is not None and hasattr(reg, 'RegisterPanelWidget'):
reg.RegisterPanelWidget(panel, 'MyTool', anchor='topleft', order=100, margin=12,
shown=bool(c.par.Showlist), active=bool(c.par.Active), tool=c,
pars={'show': 'Showlist', 'corner': 'Listcorner',
'margin': 'Listmargin', 'order': 'Listorder'})-
Register for as long as the tool exists, and say whether the widget shows:
shownis the user's switch,activethe tool's own on/off. Only a widget with both is placed. Unregister only when the panel goes away; a hidden widget stays listed in the Overlay tab of FNS_Hub (since 2026-10-12, docs/OverlayManager.md). -
toolandparsname the tool parameters behind show, corner, margin and order, so the Overlay tab writes a change where it lives and the tool re-registers from it. -
anchoristopleft,topright,bottomleftorbottomright; the tool usually offers it as a parameter (the floating parameter window covers the top right for some users). -
orderis the widget's place in its corner's stack, lowest nearest the corner, ties by name. Use the layer bands' numbers (100 for marks, as CookBar does). -
marginis the tool's, in screen points (12 by default): the widget sits that far from the editor's edges, and that far from the widget before it when several share a corner. Offer it as a parameter of the tool, beside the corner (CookBar: List Corner, List Margin). The stacking is the registry's, so two tools in one corner never overlap. -
The panel's own size is its size on screen. Size and offsets are divided by the zoom and compared with the live geometry every frame, so a drag or resize by the user is undone at once.
-
Registrations live in the global's storage (
widgets, strings and numbers only: a spec holding a nested dict and bools crashed TD natively insidestore(), 2026-10-12, soparstravels as'corner=Listcorner margin=Listmargin ...'and the flags as'1'/'0';WidgetSpecs()hands them back decoded) and the annotations are held as OP references; the presave hook, the extension teardown and the boot sweep (tagfns_network_overlay_widget) remove them, andpre_release_commonstrips the tag from artifacts. -
Keep it small. The whole rectangle takes clicks.
A layer is drawn when Displayed (the user's, bound to the tool's
Ovdisplayed and a constant) AND Layer Active (the tool's, on the
host's Registration page, never promoted) are on. Layer Active is an
expression on the tool's own switch, Active by default; QuickMarks' reads
me.parent().par.Active and me.parent().par.Showpins. Before this the tool's
switch lived in Ovdisplayed itself, so hiding a layer meant switching the
tool off and a manager writing Show through the bind flattened the
expression. _ensureLayerActive moves an old expression across on every
host's init. Reasoning and the Overlay tab that writes these:
OverlayManager.md.
The package is the COMP every host clones from; it clones nothing. It
shipped with Enable Cloning on and an empty Clone par, and in that
state any custom-par write on it crashed TouchDesigner natively: seconds
later, at the next save, or on every boot once a page of pars was in its
tox (seven crashes, 2026-10-11). Switching only that flag off, the same
writes and saves survived and a cold boot was clean; the Toolbar, Timeline
and Config masters have it off. _ensureMasterNotCloning heals it on init.
That page was a package-wide Widgets page (Margin, Gap), and it lasted
one day: the owner wanted the margin per tool, beside the tool's corner,
where Layer Order already lives. _retireWidgetLayoutPage removes it where
it was saved.
The first widget: the viewed network's heaviest operators, ranked by cook
time (only what cooked within the last second: cookTime keeps the last
cook's duration forever) or GPU memory. Tabs along the top switch the rank
(Toprank), a row click makes the operator the only selection and frames
it (homeSelected(zoom=True)), and the widget's own annotation is
deselected first, since framing it would zoom out further on every click.
The second client, and the first with both parts. The mark list is a
widget (order 150, so it stacks under the Top List in a shared corner): the
stored marks by number and name, the one for the viewed network
highlighted, a row click running RetrieveQuickmark like Ctrl+number. The
outlines are a layer (null_pins_layer, order 200, the labels band): a
GLSL TOP draws the rounded outline of each mark's saved area (centre x,
y, and since 2026-10-11 the area's size w, h in network units; older
marks take the current pane's size at the saved zoom), and a Text TOP
labels each in its top-left corner, positioned through the View like
CookBar's bars. A list row's right-click renames its mark (TD's
non-blocking PopDialog), a mark whose network is gone reads missing, and
both widgets fold to their top row from an arrow there (Mark List
Collapsed, Top List Collapsed); the registry restacks from the live
height. execute_overlay checks every frame and rewrites the
list or the pins only when what they show has changed. Mark List, List
Corner, List Margin and Pins sit on QuickMarks' Custom page; the host's
Layer Active follows Active and Showpins (Displayed until 2026-10-12).
Its release hook now empties every mark slot: the author's own marks had shipped in the tox, and the list would have shown them on a fresh install.
- A shared marks layer: partner_overlay's shader (rounded outlines, cursors, name tags) as a component any tool feeds with rectangles.
- TDCollab as a client when the toolkit is present (its own project). Done: TDXCollab registers one layer and two widgets (see below).
Toolbar buttons that switch a layer (CookBar, POPOverlay) needed a re-hover
before the next click. Every toggle rebuilt the /sys extension: TD builds
it by evaluating ext0object, and every operator read during that
evaluation becomes a dependency of the expression. _ensureOverlayOps and
_syncSurface read execute_frame (its text and Active) during
construction, and creating a layer Select TOP inside the global
re-evaluated the lookup too. A toggle flipped execute_frame, the next
access (QuickMarks' frame callback, every frame) rebuilt the extension, and
the new instance swept and redrew the overlay and widgets in the viewed
network in the middle of the click, on a frame of up to 244 ms.
The rules that keep it from coming back:
- No operator reads during construction.
postInitonly schedules_globalSetupa frame later;_syncSurfacereturns while_inInitis set. - No children created on a toggle. Layers go through a fixed pool,
layer_0..layer_5, made by_ensureOverlayOpsand re-pointed by_syncSurface. - No annotation created or destroyed on a toggle. A widget whose tool switches off is parked (viewer off, not clickable, no size) and reused on its next show; widgets are destroyed only when nothing is shown at all, the network changes, or the project saves.
Measured after: no rebuild on any toggle; the click frame went from 244 ms
to 29 ms (POPOverlay on) and from 174 ms to 38 ms (CookBar off). The other
/sys registries cooked their extension DAT once in the same session.
TDXCollab embeds a host of this registry and ships the registry unchanged:
one version, built from this master, serves both toolkits. Whichever loads
first is the global and the other's copy ties and stays out. TDXCollab
never edits or re-versions it, so its findings land here and both take the
next release. It draws one layer (partner cursors, view rectangles,
selection boxes from one GLSL TOP reading the View pars) and two widgets
through RegisterPanelWidget.
What 0.2.0 settled, from their reports:
- Non-Commercial is 1280 on the longest side, aspect kept. Measured
here under
app.addNonCommercialLimit(password=...): 2000x1000 drew 1280x640, 1280x1000 and 1000x1280 were untouched, 2000x2000 drew 1280x1280, 4000x2 was untouched. Detection readslicenses.isNonCommercial;licenses.typesaysTouchDesigner Non-Commercial(andTouchDesigner Educationalon the dev machine), so the old== 'Non-Commercial'never matched. TD then drew the composite smaller than the View pars said, and TDXCollab's cursors landed outside the image on a Non-Commercial Mac. - Widgets survive a takeover. Layers ride the base's entry table;
widgets live in their own
widgetsstorage, which the takeover did not merge, so a newer global started without them until each tool registered again._merge_pane_registry_fromnow carries them, and_dropParallelTableclears the master's copy afterwards. - A host that comes up before the global retries (every 30 frames, 20 times) instead of reporting Registered while its registration was ignored.
- The annotations enclose no operators (
encloseops = False). - 0.2.1: no self-dependency on reinit. The master's widget cleanup ran
in
postInitwithfetch('widgets'), and a fetch during construction made the COMP depend on its own storage: a cook dependency loop warning naming only the COMP after every reinit (TDXCollab saw it on their host). It readsstorage.getnow; reinit leaves no warning on master or host.
What a client can rely on, and must keep:
- Layer Active stays an expression. Every registration re-asserts the
default expression over a constant (
_ensureLayerActive), so a tool that wants its own condition writes it as an expression (TDXCollab reads COMP storage, QuickMarksActive and Showpins). - The tags are stable:
fns_network_overlayon the layer annotation,fns_network_overlay_widgeton each widget annotation. Both are utility operators thatCOMP.saveByteArraycarries, so a tool that serialises a network strips them by tag. - Pane following: the current network editor, else the one followed last while another pane type is current. A change to this rule is a change to the contract.
- The covered rectangle includes the pane's path bar.
bottomLeftandtopRightinclude the roughly 24 point bar, whilepane.x/pane.yis the centre of the network below it. Network-space positions (the View pars,NetworkToPixel) are self-consistent; anything placed in screen points from the pane centre is off by about 12 points.