Display and update text

Extend First Game’s text walkthrough with a score that changes during play. CE::Text::Rendering::Updater retains the last complete text while preparing its replacement on a tracked CPU worker and uploading through the platform dispatcher. The application supplies content and layout; the updater manages pending work and adoption.

Retain an updater

Include <cheryl/text/text-updater.h>, <format> and <limits>. Replace First Game’s score_ generation member with:

std::optional<CE::Text::Rendering::Updater> score_;
int points_ = 0;

During platform-owned init_platform(), replace its font/layout/upload block with:

score_.emplace(engine_, CE::Text::Rendering::Request{
    .text = "Score: 0",
    .layout = {.pixel_height = 24},
});

This constructor uploads the initial generation synchronously, so current() is available immediately. It captures the engine’s configured default font; no application font file or material setup is required. Configure fonts before constructing consumers. Set Request::font to a retained registered font when this label needs an override.

The optional remains empty if construction fails. The constructor uses the context’s tracked worker group and saved platform endpoint; the game needs no dispatcher member, future, or worker-management code.

Supply the desired content

The updater keeps one active replacement and only the newest desired request. The current complete text stays available while that replacement is prepared:

        flowchart TB
    accTitle: Bounded text replacement
    accDescr: The newest desired request starts when the active slot is free. One replacement prepares and uploads a complete candidate, which update adopts as current text. Frames retain the current generation independently.
    desired["Newest request"] -->|when idle| active["One active replacement"]
    active -->|adopt ready candidate| current["Current text"]
    current -->|retain| frames["Frame packets"]
    

After First Game’s gameplay updates, let Space also increment the score:

if (tick.input.button(next_cell_).pressed() && points_ < std::numeric_limits<int>::max())
    ++points_;
score_->request({
    .text = std::format("Score: {}", points_),
    .layout = {.pixel_height = 24},
});
score_->update();

request() owns the string, font, layout/raster options and optional material. Repeating the same request submits no work. update() advances completed stages without waiting; call it each simulation update even when the score is unchanged. It returns whether a replacement was adopted.

At most one replacement is active and one newest desired request is retained. If 10 is uploading while requests advance through 11 to 12, the updater adopts 10, then prepares 12. It keeps drawing the previous complete result through preparation and upload. For longer content, set layout.maximum_width to the available local width and submit the changed width when the view resizes.

Choose a failure policy

An asynchronous preparation or upload failure leaves current() unchanged. failure() exposes its stage, exception and request revision. Report an event once using that revision if your game continues running. A failure can belong to an older active request while a newer desired request proceeds.

An unchanged failed request is not retried automatically. After correcting a transient problem, call:

score_->retry();

Then continue calling update(). A distinct request proceeds normally. Call retry() in response to your application’s retry decision, rather than on every update. To use the runtime’s normal stop-and-cleanup policy instead, rethrow a reported failure with std::rethrow_exception(failure->error).

Publish in screen coordinates and release

Replace First Game’s text submission with:

CE::Rendering::DrawStyle2D style;
style.model_matrix =
    glm::translate(glm::mat4(1.0f), glm::vec3(24.0f, static_cast<float>(camera_.framebuffer_size().height) - 48.0f, 0.0f));
pass.add(CE::Rendering::Submission::resolve_text(*score_->current(), style, context));

Here context is the pass’s Context2D, which inserts atlas images automatically, as in First Game’s pass setup. The generation retains its compatible material. Published frames retain their resources when the updater adopts a new generation.

For a scrolling world, use First Game’s separate screen pass instead of sharing the world camera. During deinit_platform(), after runtime has settled accepted work and recycled frames:

score_.reset();

The updater never makes a worker wait for graphics, and its jobs own their request data rather than borrowing the game. Keep graphics dependencies alive through cleanup. An updater constructed without an initial request starts with an empty current(); check that handle before drawing until the first result is adopted.

The score should start at zero, remain visible during replacement, and keep its screen placement during resizing and world movement. Check both runtime modes. The maintained demo uses this updater for its changing direct-text HUD.

Create a HUD with TGUI builds score and health widgets; the RmlUi GUI guide covers documents and interactive editing. Read the updater contract, Unicode layout, encoding/source indices and resource publication for detailed limits.