Lucky Robots Blog Open Roles

2.1 · Application Lifecycle

Application is the singleton at the root of every LuckyEngine process. It boots the window, owns the layer stack and render-thread context, pumps OS events, and runs the frame loop that ticks every other subsystem.

Module: Hazel/src/Hazel/Core/ Application.h Layer.h RenderThread.h EntryPoint.h Singleton
Boot → Main loop main() EntryPoint.h CreateApplication() client-implemented Application::Init window + render thread Application::Run() frame loop Poll OS events Window::OnUpdate Dispatch events Layer::OnEvent Layer::OnUpdate(ts) stack, bottom → top OnImGuiRender overlays TimeManager::Tick runners + phases Drain render queue main or render thread Swapchain present nvrhi / Vulkan repeat until quit Run() repeats until WindowCloseEvent flips m_Running.
Boot path through EntryPoint.h and the per-frame work inside Application::Run().

Overview

Every LuckyEngine executable — the editor, the runtime, the headless app, the tests — is a thin shell around a single Application instance. Client code never writes main() by hand; EntryPoint.h defines it and calls into the client-supplied CreateApplication() factory. The returned Application* is then driven through its lifecycle: construct → InitRunShutdown → delete.

Once running, Application::Get() is the global handle every other system uses to reach the window, the layer stack, and the render-thread context. There is exactly one of these per process — do not try to construct your own.

Boot sequence

A LuckyEngine app starts identically on every platform. The client only fills the factory; the engine handles the rest.

StepWhere it runsWhat happens
1. main() EntryPoint.h Engine-provided entry. Sets up logging, parses common args, and invokes CreateApplication().
2. CreateApplication() Client (e.g. LuckyEditor.cpp) Builds an ApplicationSpecification — window title, size, threading policy — and returns the concrete Application subclass.
3. Application ctor Engine Stores spec, creates the Window, initialises the render-thread context, builds default textures, primes AssetManager.
4. OnInit hook Client Optional override. This is where the client pushes its initial layers (editor pushes EditorLayer, runtime pushes its runtime layer).
5. Run() Engine Loops until WindowCloseEvent flips m_Running to false.
6. Shutdown Engine Detaches every layer in reverse, joins the render thread (if any), tears down NVRHI, destroys the window.

The layer stack

Layers are the unit of feature composition. Each layer implements a small surface defined in Layer.h:

HookCalled whenNotes
OnAttach()Once, on PushLayer / PushOverlayAllocate resources, register callbacks, subscribe to events.
OnDetach()Once, on pop or app shutdownRelease resources. Must be idempotent — layers are torn down in reverse.
OnUpdate(Timestep ts)Every frameFrame-rate-bound work. Use TimeManager for fixed-rate work instead.
OnImGuiRender()Every frame, inside the ImGui passUI panels, debug overlays.
OnEvent(Event&)For every dispatched eventMark Handled to stop propagation.

PushLayer vs. PushOverlay

PushLayer

Regular layer

Inserted before the overlay region. Update order is bottom-up; event dispatch is top-down. Use for gameplay layers, editor body, debug systems that share priority with peers.

PushOverlay

Overlay layer

Always sits on top of the regular layers. Reserved for things that must see events first and draw last — ImGui itself is an overlay.

Reach for layers sparingly

Most new features want a panel (Editor), a component (Scene), or a TimeManager runner (fixed-rate sim work) — not a new layer. Layers exist for cross-cutting orchestration; piling layer-per-feature creates ordering puzzles.

The Run() frame loop

Run() is a fixed sequence per iteration. The diagram above shows it; the table below pins it down with the ordering invariants other subsystems rely on.

OrderStageWhy this position matters
1Poll OS events (Window::OnUpdate)Inputs and window state for the frame must arrive before any layer reads them.
2Dispatch events to layers (top → bottom)Overlays consume input first; Handled stops the chain.
3Layer::OnUpdate (bottom → top)Gameplay runs before UI so panels read consistent state.
4Layer::OnImGuiRenderUI build phase — ImGui draw lists are collected here.
5TimeManager::TickSteps every registered runner (physics, control, recording …) at its own fixed rate.
6Drain render command queueOn the render thread (MT) or the main thread (ST). See pitfall below.
7PresentNVRHI swapchain present + frame index advance.

Threading policy

ApplicationSpecification::CoreThreadingPolicy is the single switch that decides whether m_RenderThread is a real std::thread or a same-thread queue drain. It is set once at construction and never changes during the lifetime of the app.

PolicyUsed byRender threadQueue drain
MultiThreaded Runtime, HeadlessApplication Dedicated worker thread Render thread drains while main builds frame N+1
SingleThreaded Editor (LuckyEditor.cpp) Same as main Main thread drains at end of frame
Critical for AI agents

In the editor, main and render are the same thread. Lambdas pushed via Renderer::Submit(...) do not cross a thread boundary, do not need extra synchronisation, and can capture editor-side state directly. In the runtime, the same call does cross a thread boundary — capture by value and treat shared state as needing a lock. See .claude/docs/Threading.md § "Editor vs. runtime".

TimeManager integration

Application::Run calls TimeManager::Tick exactly once per frame. Everything that wants to run at a fixed cadence — physics at 500 Hz, control at 100 Hz, recording at 30 Hz — registers a callback with TimeManager rather than piggy-backing on OnUpdate.

Phases in execution order: AcquisitionControlPhysicsValidationExport. A free-update phase (FreePhaseID::PostExport) runs once per refresh-rate frame for UI-paced work. The full contract lives in .claude/docs/Threading.md.

Watch out

Don't add per-frame logic to Application::Run or to Scene::OnUpdate. Both are protected files. Register with TimeManager instead — you pick the phase and the frequency, and the loop already calls you.

Extending

Most new work fits one of these shapes:

You want…Do this
Code that runs every visual frameImplement Layer::OnUpdate on an existing layer.
A fixed-rate system (physics, recording, telemetry)TimeManager::RegisterWithRunner with the right phase.
UI in the editorA new EditorPanelnot a new layer. See 2.10 Editor.
A cross-cutting feature (input, agent, debug UI)A new layer pushed in the relevant client's OnInit.
Process-wide stateReach for it via Application::Get(); do not introduce another singleton.

Pitfalls

Layers detach in reverse

If layer B was pushed after layer A and B's OnAttach registered a callback on A, B's OnDetach must unregister before A goes away. The stack tears down top-to-bottom so this is the natural order — don't fight it with manual ordering.

Don't capture Application& by reference in a worker

Application::Get() is fine on the main thread. From a std::jthread, prefer marshalling back via a dispatcher — see Threading.

The threading switch is not dynamic

CoreThreadingPolicy is read once during construction. There is no "go MT for this frame". If you need both behaviours, you need two apps (e.g. runtime and editor share Hazel; they don't share an Application instance).

Key types

TypeHeaderRole
ApplicationHazel/src/Hazel/Core/Application.hSingleton root. Owns window, layers, render-thread context, event queue.
ApplicationSpecificationApplication.hPOD config — title, size, threading policy, working directory.
LayerLayer.hBase class for the layer stack hooks.
LayerStackLayerStack.hManages layer + overlay regions and iteration order.
RenderThreadRenderThread.hReal worker thread when policy is MultiThreaded, same-thread shim when SingleThreaded.
TimeManagerTimeManager.hFixed-rate phase scheduler. The home for all sim-rate work.
EntryPoint.hCore/EntryPoint.hDefines main(). Included by exactly one TU per executable.