Axiom/SDK
SDK / Overlay and canvas

Overlay and canvas

Two ways to put something on screen: a retained widget tree that looks like the rest of Axiom, and a draw list for mods that want their own pixels.

overlay@1

A retained widget tree. You describe it; the host keeps it and renders every frame without asking again. What crosses the boundary is the diff, not the draw calls.

Answered by: the backend
Why retained rather than immediate mode

An immediate-mode API is pleasant to write and the wrong shape here: every Text() call would be a crossing, so a panel with sixty rows would cost sixty crossings every frame. With a retained tree, a frame where nothing changed costs zero.

ui.panel(title: str) -> Panel

A top-level panel. Gets its own tab in the overlay.

panel.list() -> List

A list whose contents you replace wholesale.

list.set(rows: [Row])DIFFED

Replace the contents. Diffed host-side, so an unchanged list costs nothing.

void frame(Framework& f) override {
  list.set(rows);  // 1 crossing, and only if rows changed
}
panel.slider(label: str, min: f32, max: f32, bound: key)

Bound to a config key, so persistence and validation are free.

Parameters
bound
A key declared in [config]; the value is clamped to its declared range
panel.button(label: str, id: str)

Fires through event() on the mod’s thread, next frame. It does not fire inside the click, which matters if you expected to read input state in it.

panel.row(fmt: str, args...)

One row. Format args are marshaled once.

panel.text(fmt: str, args...)

A line of text.

panel.table(cols: [str]) -> Table

A table with headers; set() like a list.

panel.toggle(label: str, bound: key)

A checkbox bound to a config key.

panel.select(label: str, options: [str], bound: key)

A dropdown.

panel.color(label: str, bound: key)

A color picker.

panel.graph(label: str, value: f32, window: u32)

A rolling series. The host keeps the ring buffer, so you push one value a frame rather than an array.

panel.group(label: str) -> Group

Layout only, no behavior.

panel.collapse(label: str) -> Group

Collapsible, state remembered per user.

panel.note(text: str)

Quiet explanatory text. Use it to say why something is unavailable.

Styling is not yours

Panels inherit the overlay’s tokens so twelve mods look like one application. A mod cannot set a font, a background or a corner radius. If you need your own pixels, that is canvas, deliberately a separate thing.

canvas@1

A draw list, not a draw API. Commands accumulate in your own memory and cross the boundary once, at submit.

Answered by: the backend
canvas.begin(layer: Layer) -> DrawList

Start a list. Nothing has crossed yet.

Parameters
layer
World under the game’s HUD, Hud over it. Overlay is denied: a mod that could draw over Axiom’s panels could cover the panel used to disable it.
d.submit()ONE CROSSING

One crossing, whatever the list length. Twelve shapes or twelve thousand costs the same.

auto d  = f.canvas().begin(Layer::World);
auto ss = f.project().world_to_screen_many(ps);

for (auto& s : ss) {
  if (s.failed()) continue;
  d.rect(s->x - 20, s->y - 40, 40, 80, Color::accent, 1.5f);
}
d.submit();  // 4 crossings for the whole frame

Draw commands

DRAW LISTcanvas@1
line(a, b, color, width)          rect(x, y, w, h, color, width)
rect_filled(x, y, w, h, color)     circle(c, r, color, width, segments)
poly(points, color, width)        poly_filled(points, color)
text(x, y, s, align, size)         image(x, y, w, h, handle, tint)
push_clip(rect)                    pop_clip()
push_transform(mat)                pop_transform()
Detail
Rule
Color
sRGB. The host converts for the swapchain, so you do not read color_space() unless you tone map yourself.
Text
Host-rendered from the overlay’s atlas; you ship no font. size is DPI-scaled pixels.
measure_text
Available host-side for layout. One crossing, worth caching.
Images
Upload once at init, draw by handle. Uploading per frame costs the whole frame, and the profiler names you.