MIT License NuGet

Home / Docs / Apps and panels

Applications, panels and the frame loop

BaseApp derives from Panel. Your application is the root of the panel tree, which is why AddPanel and AddControl work on it directly and why there is no separate scene graph type to learn.

The order things happen in

StageWhat is safe here
Your constructorTitle, resolution, camera, StorageService.DirectoryBase, and every RenderQuality.Default* value. No GPU exists yet.
Init()Called by the platform runner once the device and swap chain exist. Override only if you need to bracket initialization.
Create()Build the panel tree, load fonts, register compute effects. Call base.Create() first.
Update(time)Once per frame. Advance your own state; the pipeline reads the result.
Resize() / ResizeContent()Called on window or orientation change. Re-place your 2D layout here.
Draw()Owned by the engine. You do not normally override it.

Create is declared async void, which is unusual but deliberate: it lets you await a font or a settings read while the first frames are already being presented. Nothing in the pipeline waits for it, so a control added three frames late simply appears three frames late.

Panels

A panel owns controls and other panels. There is no layout system, no docking and no constraint solver — a panel is a place to put related things and a place to put the code that drives them.

MemberNotes
Controls, PanelsPlain lists. You may read and reorder them; use the Add/Remove methods to mutate.
AddControlAdds a leaf control and queues its load. Returns false if it was already there.
AddPanelAdds a child panel. It does not queue a load — a panel with real loading work is queued explicitly, see below.
RemoveControl / RemovePanelDetach and dispose the child. If you want to detach and keep it, remove it from the list yourself.
Layer, OrderDraw order within the parent. Ties fall back to insertion index, so ordering is a stable total order.
AlphaMultiplies down the subtree. Alpha = 0 prunes the whole branch from every pass — the cheapest way to hide a screen.
SetMode(string)A one-string hook for panels with variants, so you do not need a subclass per state.
OnCloseInvoked when the panel closes itself; how a dialog tells its owner it is done.
IsDisposedSet by Dispose. The load queue checks it, so disposing mid-load is safe.
Drawing recurses. Updating does not.

Panel.Draw walks its controls and child panels for you, sorted, in every pass. Panel deliberately does not override Update: you update the children you own, in the order you want, in your own Update. That is why update order is something you can reason about — and why a control you forgot to update still draws, at its last state.

The one Update signature

Every control and panel has the same shape. Call base.Update, then drive whatever you own. The optional arguments let a parent override placement for one frame without mutating the child's fields.

public override bool Update(float time, float? alpha = null,
    float? posX = null, float? posY = null, float? posZ = null,
    float? width = null, float? height = null, float? depth = null)
{
    var result = base.Update(time, alpha: alpha,
        posX: posX, posY: posY, posZ: posZ,
        width: width, height: height, depth: depth);

    // time is the delta in seconds since the previous frame.
    angle += time * 0.5f;

    robot.Rotation = angle;
    robot.Update(time);

    return result;
}
PointWhy it matters
time is a deltaSeconds since the last frame, not an absolute clock. BaseApp.Time is the accumulated total if you need a phase.
Returning trueMeans the frame was consumed — a click was handled. Interactive panels use it to stop propagation.
No Draw to writeUpdating state is the whole contract. There is no draw call, no command list and no material binding in application code.
Input arrives hereOnClick and OnTouch fire from inside BaseControl.Update, which is why a control that is never updated is also never clickable.
Always pass these by name

The base declaration orders the parameters posX, posY, posZ, width, height, depth, but several control types declare their override as posX, posY, width, height, posZ, depth. The types are all float?, so the compiler is satisfied and each override forwards to its base by name, which keeps behaviour correct — for named callers. A positional call past alpha can silently put your Z into Width. Pass named arguments, or skip the arguments entirely and assign the properties before calling Update(time).

Loading is a queue, and Ready is the flag

Nothing loads synchronously. AddControl puts the child on a global queue through BaseApp.RequestLoad; the queue calls Load() with a concurrency limit, and on success sets Ready = true and stamps LoadComplete.

  • A failed load is not retried. Load returning false leaves the control not ready, and nothing throws. An invisible model is usually a wrong asset path.
  • Ready means different things by kind. For a leaf control it means GPU resources exist and it is drawable; for a panel it means only the panel's own load finished, while children keep becoming ready behind it.
  • Disposal during load is safe. The queue checks IsDisposed before and after, and discards the result rather than resurrecting a dead control.
  • Queueing is deduplicated. Requesting the same loadable twice is a no-op that returns false.
  • The web build spends a per-frame budget. A browser has no synchronous file access, so loads are metered across frames instead of blocking — the reason a large scene fades in progressively there.
// Wait for something before using it, without blocking the frame:
if (robot.Ready)
    robot.Update(time);

// A panel that does real loading work of its own opts in by declaring
// the interface; the base class already provides every member.
internal class Rocks : Panel, ILoadable
{
    public override async Task<bool> Load() { /* extract in the background */ return true; }
}

// AddPanel does not queue, so queue it yourself:
AddPanel(rocks);
DeviceServices.BaseApp?.RequestLoad(rocks);

The split is deliberate: ILoadable is what the load queue recognises, IControl is what the render passes recognise. A panel is loadable but not drawable; a sprite is both. Because a panel only declares ILoadable when it genuinely has work to do, the type system prevents you from queueing containers by accident.