Skip to content
CppCooperProgramming Portfolio

Article

Prepared Frames: From Game State to Rendering

ArchitectureGraphics

A renderer needs a stable description of what to draw while the game is free to advance. Cheryl Engine 4 makes that description an explicit frame: copied pass values and resolved draw packets held in a reusable slot.

Simulation prepares the slot, then the runtime transfers permission to read it. The graphics owner renders it and eventually recycles it. The slot’s data and the handoff protocol solve different parts of the problem.

Copy decisions; retain the resources they use

A CE::Rendering::Pass owns projection and view matrices, custom parameters, pipeline constraints and an ordered vector of DrawPacket2D values. The camera is consulted during preparation. Playback receives its copied matrices.

A packet holds geometry and an immutable material generation through retained handles. Its parameters contain resolved transforms and retained image/sampler values, together with the selected vertex range. The renderer does not need the actor, sprite wrapper or animation cursor that produced the packet.

This separates two kinds of lifetime. The actor’s transform and selected animation cell are decisions made for this publication; the uploaded geometry and material are resources that must survive until it is no longer needed. Retaining them does not keep a closed graphics domain usable. The resource-lifetime article follows that distinction further.

Prepare one actor’s draw

This C++ fragment is the sprite section of First Game’s prepare_render_frame(). The runtime supplies frame; the method uses the game’s existing camera_, initialized actor_, material images_ and actor_scale_ members. The complete method also adds the score text.

auto pass = frame.begin_pass(
    camera_.projection_matrix(), camera_.view_matrix());

const CE::Rendering::Submission::Context2D context{
    pass.semantics(), pass.parameters(), pass.constraints()
};

CE::Rendering::DrawStyle2D image_style;
image_style.material = images_;
image_style.scale = actor_scale_;
image_style.model_matrix = glm::translate(
    glm::mat4(1.0f), glm::vec3(actor_->position, 0.0f));

pass.add(CE::Rendering::Submission::resolve_sprite(
    *actor_->sprite, actor_->cell, image_style, context));

resolve_sprite() checks the grid cell and selects its geometry range. It uses the explicit style material when supplied, otherwise the asset’s retained default. Image insertion and material resolution produce the packet’s owned parameter values. No graphics upload or draw occurs in that step.

PassWriter::add() validates the packet and assigns its authored position. If the actor moves, the camera changes or animation advances afterward, the submitted values still describe the earlier selection. The next publication can describe the new one.

The writer is borrowed only during prepare_render_frame(). Keeping it in the game would preserve access to a slot whose owner is about to change. Its destructor does not publish anything; publication belongs to the runtime.

Transfer a slot instead of copying a frame

Frame is neither copyable nor movable. Concurrent execution owns three slots and tracks their states under the scheduler mutex. A worker selects a Free slot and marks it Writing, then prepares it outside the lock.

Only after preparation returns does the worker mark that slot Ready. If an older completed frame is still waiting, it becomes Retired. Platform claims the newest waiting frame when rendering becomes eligible and marks it Rendering.

For a concrete sequence, suppose A is the current displayed frame, B is waiting Ready and simulation finishes C. Publishing C retires B. Platform can release B without ever drawing it, then replace A with C at a render handoff. These letters identify reusable storage slots; they are not three copies of the live game.

If there is no Free slot, simulation skips preparation for that batch and keeps advancing authoritative state. It does not wait for the renderer to return a slot. Consequently, this protocol favors a recent completed view over presenting every intermediate update.

The platform keeps its current complete frame between publications and can render it again. Sequential mode applies the same data boundary with one recycled slot, since preparation and playback occur on the same caller. The runtime-lifecycle article explains those execution owners.

Prepared-frame slot ownership
A complete Ready frame may be rendered or superseded. Platform owns recycling. Preparation failure aborts the session and leaves partial data for shutdown cleanup; it does not publish that data.

Recycling is part of ownership

The simulation worker does not clear a superseded slot immediately. Platform changes Retired to Recycling, calls Frame::recycle(), then returns it to Free. That clears the active packets and pass parameters, releasing their retained handles on the graphics owner.

The pass objects and vector capacity remain available for later preparation. Reuse therefore keeps storage across updates, while still allowing allocations when capacity grows. Three slots also retain more storage and resource references than the sequential slot.

Platform maintenance continues even without a fresh frame. The OpenGL renderer’s maintenance collects deferred resource retirements. Merely waiting for the next draw would leave that work dependent on the game publishing again.

Where preparation and playback can fail

A new FrameWriter rejects a slot with active, unrecycled passes. Packet validation rejects missing handles, incompatible ranges or pass state, and incomplete parameters. If preparation throws after adding some passes, writer destruction does not undo them. Runtime failure cleanup recycles the partial frame before game graphics cleanup and backend teardown; it is never published as Ready by that failing path.

Writers themselves provide no cross-thread synchronization. Even during preparation, adding passes can grow the outer vector and invalidate borrowed parameter references or pass spans. A PassWriter retains an index, but references obtained through it still have their own shorter lifetime.

CPU validation also cannot establish the current native context. During playback the OpenGL renderer checks that the packet’s pipeline belongs to its resource domain, then draws passes and packets in authored order. A backend failure may leave part of a frame drawn or native state changed. Rendering is not a rollback transaction.

The handoff contract is therefore precise: one owner prepares stable values, another consumes them, and recycling releases their dependencies before storage returns to the writer.

Cheryl Engine 4 reading path

  1. Runtime ownership and lifecycle
  2. Prepared frames and rendering handoff (this article)
  3. Retained resources and cleanup
  4. Input routing and editable menus
  5. Unicode layout and retained text
  6. Optional modules and the installed SDK