Runtime frame boundary

CE::Runtime::GameRuntime coordinates input polling, simulation, rendering, presentation, and shutdown. Both execution modes use the same configurable timing policy, with variable/fixed steps and bounded direct/hybrid recovery. The application chooses a compatible platform, graphics context, renderer, resources, and input adapter. The renderer uses the selected context and render target; it does not own the display system or read live game state. The presentation surface presents a completed frame. The GLFW/OpenGL factory accepts the game’s window configuration, constructs the display and window, and lends the window to its presentation context. The display owns the GLFW library lifetime; renderer initialization makes the context current and loads OpenGL entry points before game initialization uploads assets.

Input and owner threads

Each platform poll produces a timestamped CE::Input::Actions::Snapshot. Each independently scheduled update() consumes the complete pending batch in one CE::Input::Tick. Input supplies edges, counts, and observed hold durations; it never subdivides simulation time or schedules extra updates. Simulation produces complete render state, which remains stable while rendering consumes it. The renderer must never access mutable simulation objects across that boundary. Each complete CE::Input::PollSnapshot carries State and independently captured ordered Events/Text records. The runtime transfers both at the same consumption boundary; CE::Input::Tick::records() retains their shared observation order. Scoped capture requests and backend fidelity are documented in input-state-model.md. Each tick also carries logical window and framebuffer dimensions sampled together on the platform owner. Simulation uses these copied values for UI layout and pixel scaling without reading the live window. Manually constructed ticks supply their logical dimensions explicitly; the default is zero. These rules also apply when simulation and rendering run sequentially. The sequential runtime owns one reusable frame. Concurrent mode owns three slots: one may be rendered, one may be the latest completed frame, and one may be in preparation. Platform event polling and graphics-context use respect their respective thread affinity.

The caller of CE::Runtime::GameRuntime::run() owns platform polling and the graphics context in both modes. Sequential mode polls, updates, prepares, renders, and presents on that thread. Concurrent mode runs only update() and prepare_render_frame() on one simulation worker; it passes complete polling batches and framebuffer dimensions as values instead of giving that worker access to a live window. A resize callback runs on the platform thread and must not mutate simulation state. init() runs after input and graphics initialization but before starting the worker, so it can register bindings and load GPU assets. After the worker joins and frames are recycled, deinit() runs while the graphics context is current. The default GLFW/OpenGL factory owns its input adapter; the overload taking an input reference borrows it through context destruction. Owned input detaches before the window is destroyed. A runtime and its context each permit one session. Game cleanup is paired with attempted initialization, including partial failure; cleanup preserves the first failure while still shutting down both adapters and reports subsequent failures with phase context. See failure-reporting.md for deferred native callback checks and the bounded emergency reporting contract.

Queued preparation and stop requests

engine.platform_dispatcher().submit(work) transfers owned request data to the platform thread, where graphics is current. The result is a future: inspect readiness from update(), then publish the resulting handle through the next render frame. Do not block simulation on it or capture live simulation objects in platform callbacks. The queue drains FIFO batches before polling, including while the input backlog is full; nested requests wait for the next batch. Futures, cancellation and saved submission endpoints follow the dispatcher contract. Accepted dependencies settle under the shutdown sequence below. Callbacks must preserve the session’s current graphics context. Backend resource guards reject another context even when it is selected on the correct thread. Game code calls tick.request_stop() to request graceful shutdown from update(). Every CE::Game::TickContext shares the session’s std::stop_source with CE::Runtime::GameRuntime::stop(); both entry points wake scheduler waits and are safe to repeat. The request ends further simulation updates and uses the normal join, quiesce and cleanup sequence. It does not interrupt the current update(); return early when further game work is unnecessary. Sequential execution can still prepare and present its final updated frame before teardown. A copied tick.runtime_stop retains only stop state and remains safe after runtime destruction; do not retain the tick itself or its borrowed input view. Manually constructed ticks default to no runtime stop source; request_stop() then throws failed_operation. Tests or another scheduler can supply a source explicitly.

Frame publication and recycling

CE::Game::AbstractGame::prepare_render_frame(writer) runs on the simulation thread after the final useful update of a bounded batch when a slot is free. If all slots are occupied, the update still advances the simulation and only that tick’s visual snapshot is skipped. The runtime lends the game a reusable CE::Rendering::Frame slot through the writer. The game fills ordered passes with copied camera values, custom pass parameters, constraints, and resolved CE::Rendering::DrawPacket2D values. Asset helpers select geometry ranges and lay out text on simulation, resolving transforms, engine semantics, custom parameters, and image requests into owned values. Packets retain geometry and material generations; no asset, font interface, string, animation cursor, or live entity is needed by playback. The caller model carries placement/rotation; CE::Rendering::DrawStyle2D::scale also scales local geometry and glyph offsets explicitly. The runtime publishes the complete slot without copying it and prevents further mutation while the renderer consumes it.

Recycling a superseded or shutdown slot clears only the active passes’ draw packets and pass parameters, releasing their resource handles on the graphics thread while its context is current. The platform retains its latest complete slot between publications. Recycling keeps pass objects and draw-vector capacity for later ticks. One slot suffices when update and render are sequential; concurrent update/render need at least two. A third permits a completed frame to wait while one is being written and one is being rendered. Text strings may still allocate, and the final release of a GPU-backed handle must occur on the graphics thread or through backend-managed deferred destruction.

At each concurrent render handoff, platform claims the newest completed slot. If simulation supersedes a waiting frame, platform recycles that older slot before lending it out again. Rendering consumes authored packet order and complete validated pipeline state. The render contract owns packet resolution and writer rules; asset playback and Unicode text resolve their values before publication.

Polling admission and fresh-batch consumption follow the input contract. Frame preparation occurs once after a bounded update batch under the timing policy. Retirement maintenance runs independently of new frames under the residency contract. The demo guide owns its options, controls and application bootstrap.

Shutdown

Stopping closes context CPU-group acceptance and stops simulation. Simulation cancels its pending dispatcher work on its owner before publishing completion; initialization failure before owner binding cancels on the initializing thread. Platform keeps pumping accepted dependencies while joining simulation, so an accepted callback awaiting platform work cannot strand it in a blocking join.

After simulation joins, game.quiesce() stops external producers and invalidates borrowed event registrations while their targets/resources remain alive. Do not block this hook on work that still needs platform dispatch. It pairs with attempted game initialization, including partial startup. Accepted CPU work then settles while platform continues servicing dependencies; bounded 1 ms shutdown waits observe completion without borrowed runtime wake callbacks in workers. Only context-created groups close; an injected pool’s unrelated groups remain open. An owned root joins before game/resource cleanup.

Remaining platform requests cancel, all retained frames recycle on graphics, and game.deinit() runs before input and graphics shutdown. Preserve the first failure while completing cleanup; report later failures with phase context through failure reporting. Owned input detaches before window destruction, and the context must outlive its renderer. External logical handles follow the native lifetime contract: they can survive for destruction/CPU inspection but cannot draw through a closed domain.