Consuming the engine

Start with the first-game walkthrough for a maintained application covering startup, controls, asset loading, and sprite/text submission. This page describes the source and SDK consumption contracts.

Applications can compose source targets from a checkout or link a prebuilt development SDK. projects/engine/ owns the neutral Cheryl::Engine target, backed by the build target cengine and archive output name cheryl-engine. Applications link selected integration targets explicitly; the module guide describes ownership, standalone composition and tests.

set(CHERYL_BUILD_TESTS OFF)
set(CHERYL_BUILD_DEMO OFF)
set(CHERYL_BUILD_NATIVE_GLFW OFF)
set(CHERYL_BUILD_DEBUG_TERMINAL_LINUX OFF)
set(CHERYL_BUILD_OPENGL OFF)
set(CHERYL_BUILD_UI_TGUI OFF)
set(CHERYL_BUILD_UI_RMLUI OFF)
set(CHERYL_BUILD_AUDIO_MINIAUDIO OFF)
add_subdirectory(path/to/cheryl-engine cheryl)
add_executable(application main.cpp)
target_link_libraries(application PRIVATE Cheryl::Engine)

For native graphics, enable Native GLFW, OpenGL and native input, then link Cheryl::Engine, Cheryl::NativeGLFW and Cheryl::OpenGL. Select and link Cheryl::UI::TGUI, Cheryl::UI::RmlUi or Cheryl::Audio::Miniaudio when needed. For command-line startup, link Cheryl::OpenGL::Startup; it supplies the common startup target and the native/graphics targets transitively. Consumers obtain C++23, includes, logging policy and SDK requirements through targets rather than compiling Engine sources or repeating dependency lists.

Prebuilt development SDKs

Extract an SDK and select its root through CMAKE_PREFIX_PATH or Cheryl_DIR=<sdk>/lib/cmake/Cheryl. No Cheryl implementation checkout is needed to consume it. Link the same public aliases as source consumers:

cmake_minimum_required(VERSION 3.28)
project(my_game LANGUAGES CXX)
find_package(Cheryl 4 CONFIG REQUIRED)
add_executable(my_game main.cpp)
target_link_libraries(my_game PRIVATE Cheryl::Engine)
# Add Cheryl::OpenGL::Startup or the selected module/support targets as needed.

For a Linux Release SDK, configure the application’s directory as follows:

(
  set -e
  cd /path/to/my_game
  cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_PREFIX_PATH=/absolute/path/to/extracted-sdk
  cmake --build build --parallel
)

The SDK contains compiled Engine, Startup and selected module/topology libraries, their dependency libraries or headers, the crash-bootstrap object, public headers, CMake import/version files and licenses. .h/.hpp and required .inl/.ipp/.tpp files remain included. C/C++ implementation translation units, tests, artwork, applications and the demo are excluded. There is no separate demo package. Header-based dependencies such as miniaudio still require their public header. The build guide describes production; SDK-MANIFEST.json records selected features, source/submodule revisions, OS, architecture, compiler/version, configuration, logging profile and native versions.

Use a package compatible with the target OS/distribution, architecture, compiler, standard-library ABI and configuration. Windows CI SDKs use MSVC x64 RelWithDebInfo and the DLL CRT; Linux CI SDKs use Ubuntu 24.04 x64 GCC 14 Release. A locally built SDK’s identity describes that local toolchain, not those hosted configurations. Imported logging definitions are fixed to the producer’s compiled profile/mask; the application must not select contradictory definitions. Match MSVC runtime and configuration rather than mixing incompatible Debug/Release C++ binaries.

Native dependencies

Consumer requirements

Linux

Install compatible FreeType, HarfBuzz and ICU development/runtime packages; hwloc when selected, the producer’s Backward resolver libraries, and X11/OpenGL when selected. Pinned C++ and selected integration dependencies are already supplied.

Windows

The dependencies/ prefix supplies pinned vcpkg headers, import libraries, CMake files, release DLLs and licenses. Keep it with the SDK; add its bin/ directory to the application’s DLL search path. The MSVC runtime remains a host requirement.

The package config discovers native libraries, checks the producer’s ICU major version and requires the hwloc 2.5+ (2.x) API when that provider is included. Windows config prepends the bundled dependency prefix and exposes Cheryl_RUNTIME_DIRECTORIES for runtime-path setup; it does not deploy DLLs beside an application automatically. OpenGL still needs the host’s graphics runtime. SDK relocation does not establish compatibility with another ABI or distribution.

Installed-target consumers and first-include probes cover the selected SDK composition, including both Startup support headers, on the selected hosted Linux and Windows toolchains. The CI guide retains hosted skipped-case limits and the pending main-push artifact collection. Desktop, GPU and physical-device coverage remain separate. The SDK does not export the Linux Debug terminal; that module retains its source-composition path.

Command-line startup

CE::Application::Startup owns CLI11 parsing and validates execution, input polling and simulation timing before invoking a selected backend factory. Link Cheryl::Startup for a custom backend or Cheryl::OpenGL::Startup for the GLFW/OpenGL factory and options. The support targets expose CLI11 in their public headers; linking Engine or OpenGL alone retains their direct context/runtime construction APIs without that public dependency.

Create startup on the platform owner, register application options with add_application_options, then call initialize(argc, argv) once. Option-definition callbacks run immediately; any values bound to the parser must remain at a stable address through initialization. Startup itself cannot be copied or moved because its callbacks bind its configuration.

Help and parse/configuration errors produce a CE::Application::StartupResult with no context. Return its exit_code when should_start() is false; help succeeds and invalid arguments fail before native context construction. A successful result owns the CE::Runtime::Context and the parsed CE::Application::RuntimeOptions. Construct the game from that context, then call result.make_runtime(game) to apply its run mode, polling and timing together. Keep the result and game alive through runtime destruction; make_runtime requires an lvalue result. Backend construction failures propagate, and CE::Runtime::GameRuntime::run() retains the runtime’s existing initialization/cleanup contract.

CE::Application::StartupOptions supplies application defaults through execution and runtime. The common Engine options override those defaults:

Argument

Effect

--concurrent

Selects a separate simulation owner thread.

--fixed

Selects fixed-step simulation.

--fixed-step-ms=N

Selects fixed simulation and sets its positive step in milliseconds.

--variable-interval-ms=N

Sets variable-update spacing; zero permits unpaced updates.

--max-fixed-updates=N

Sets a positive limit on ordinary fixed updates per scheduler turn.

--variable-catch-up

Selects fixed simulation with bounded variable catch-up recovery.

--recovery-prefix=N

Sets the fixed-update prefix before recovery, at most max_fixed_updates.

--recovery-cap-ms=N

Caps the recovery duration; zero removes the cap.

--input-unlimited

Selects unlimited completed polls between simulation updates.

--input-capacity=N

Selects finite input polling with a positive completed-poll capacity.

--input-spacing-ms=N

Sets minimum spacing between completed polls; zero removes the delay.

--worker-count=N

Sets capacity for the lazily created Engine CPU pool, separate from the simulation thread. Owned capacity must be positive; an injected shared pool retains its own capacity.

Numeric values are nonnegative and must satisfy the linked timing and polling contracts. Option callbacks apply in parse order: --input-unlimited --input-capacity=4 selects finite polling; reversing them selects unlimited polling. Without overrides, the default configuration is sequential variable simulation, lockstep polling and one lazy CPU worker.

A custom CE::Application::StartupBackend provides a required create(const CE::Runtime::ExecutionOptions&) factory, optional Backend argument definitions and optional validation. The factory must return an owned context; a null result fails startup. Backend selection is currently programmatic. CLI backend selection, application-preference/build-default resolution and runtime-loaded discovery remain unimplemented at the selection point after parsing. The demo option helper shows application-specific registration without duplicating the engine parser.

Game hooks and ownership

Implement CE::Game::AbstractGame and lend it, with an CE::Runtime::Context, to CE::Runtime::GameRuntime. Keep both alive through CE::Runtime::GameRuntime::run() returning or throwing. The selected factory assembles display, window, input, presentation, renderer and resources; CE::Runtime::GameRuntime::run() initializes those adapters before calling the game.

Hook

Owner

Responsibility

init()

Platform/graphics

Bind actions and create initial resources after adapter startup.

update(tick)

Simulation

Advance game state using delta_seconds and the tick’s input/copy of window sizes.

prepare_render_frame(writer) const

Simulation

Resolve complete ordered passes/packets; publish no borrowed game state.

quiesce()

Platform after simulation joins

Stop external producers and invalidate borrowed listeners; keep their dependencies alive.

deinit()

Platform/graphics

Release application resources after accepted work settles and frames recycle.

Sequential mode runs all hooks on the calling thread. Concurrent mode gives update/frame preparation to one simulation worker; platform polling, uploads and presentation remain on the caller. Cleanup pairs with attempted initialization, so hooks must tolerate partial startup. The frame boundary defines shutdown and failure ordering.

Minimal native application

With Native GLFW, OpenGL and native input enabled, this opens a window and stops when Escape is pressed. It submits no draws yet:

#include <cheryl/backends/opengl/startup.h>
#include <cheryl/core/controls/input-interface.h>
#include <cheryl/core/game-framework/abstract-game.h>
#include <cheryl/core/game-framework/game-runtime.h>
#include <gainput/gainput.h>

class Game final : public CE::Game::AbstractGame {
    CE::Runtime::Context& engine_;
    static constexpr CE::Input::Actions::Id quit_{1};

public:
    explicit Game(CE::Runtime::Context& engine) : engine_(engine) {}
    void init() override {
        auto& input = engine_.input();
        static_cast<void>(input.bindings().bind_button({input.keyboard_id(), gainput::KeyEscape}, quit_));
    }
    void update(const CE::Game::TickContext& tick) override {
        if (tick.input.button(quit_).pressed())
            tick.request_stop();
    }
    void prepare_render_frame(CE::Rendering::FrameWriter&) const override {}
    void deinit() override {}
};

int main(int argc, char** argv) {
    auto startup = CE::Application::GLFWOpenGL::make_startup("My game", {.title = "My game"});
    auto result = startup.initialize(argc, argv);
    if (!result.should_start())
        return result.exit_code;
    Game game(*result.context);
    auto runtime = result.make_runtime(game);
    runtime.run();
    return result.exit_code;
}

Link this example to Cheryl::OpenGL::Startup. Pass --concurrent to select the simulation worker, or use the common timing/polling options above. Applications that construct CE::Runtime::GameRuntime directly can still pass RunMode::Concurrent as its third argument. Keep native binding IDs inside bootstrap; gameplay reads semantic CE::Input::Actions::Ids. Never change bindings or query a live window from concurrent update().

To add presentation, follow asset loading and pipeline/material construction in init(), then use CPU submission helpers in frame preparation. For later replacements, prepare owned data on workers, submit upload to platform and adopt only a ready complete result during update. Keep the prior generation while work is pending. The demo implementation shows that flow.

Use input actions and focus for controls, simulation timing for fixed/variable policies, and asset playback for per-entity cursors. The Unicode service prepares retained text; UI adapters keep their toolkit authoring APIs. Audio is separately application-owned and needs its producers settled before closure. World storage, entities, collision and mechanics belong to the game or its selected modules.

Dependencies and headers

Requirement

Target scope and reason

C++23, Threads, logging configuration

Engine public requirements used by templates/runtime consumers.

spdlog, CTTI, GLM

Engine public requirements; legacy glm.hpp spelling remains supported.

Backward::Interface

Engine public trace/resolver requirements; its own global signal-handler object is not linked.

STB and JSON

Engine private implementation include directories.

FreeType, HarfBuzz, ICU uc/i18n

Engine private font/layout implementation and link requirements; no library-native types appear in public headers.

CLI11

Startup support’s public parsing API; reuse CLI11::CLI11 or initialize the pinned extern/cli11 source. Plain Engine/OpenGL consumers do not inherit CLI11 headers or linkage.

GLFW

Native GLFW private implementation requirement, also used privately by the OpenGL context binding.

Gainput

Native GLFW public requirement when CHERYL_NATIVE_INPUT is enabled; its types occur in that owner’s headers.

X11

Native input’s Linux dependency; OpenGL acceptance also uses it for explicitly selected X11 scenarios.

GLAD and OpenGL::GL

OpenGL public generated types and private system link requirement.

GoogleTest

Test-only; disabled owners do not discover their integration SDKs.

miniaudio

Optional audio module’s private implementation/link requirement; public Engine and module audio headers expose no SDK types.

hwloc 2.5+ (2.x)

Optional WorkerTopology::Hwloc private SDK/final link requirement; Engine alone does not discover or link it.

The Engine umbrellas and granular contracts are neutral. Concrete window/input headers belong to Native GLFW; backends/opengl headers belong to OpenGL. Existing cheryl/... and legacy core/.../assets/... include spellings are preserved through each target’s public roots, including in the installed SDK.

<assets.h> and <core.h> expose no Glad types; concrete OpenGL resources are opt-in through <backends/opengl.h>. Portable asset values and primitives live in assets/types/, with shared 2D bases under assets/types/2d/base/. CPU geometry builders live in assets/geometry/, parsed grids/clips/manifests in assets/definitions/, and neutral upload/resource interfaces in assets/resources/. core/rendering/ owns renderer contracts, frame submissions and resolved packets. Common submission uses public parameter keys; the backend mapping owns native uniform names and graphics types.

Crash and exception traces

Cheryl::Engine delivers the automatic crash bootstrap. Final consumers receive its object directly, so a static linker cannot omit an unreferenced archive initializer. Installation follows the bootstrap object’s NDEBUG scope in the engine build: Debug-style builds install backward::SignalHandling; NDEBUG builds do not. Exception and explicit stack capture remain available through the existing bounded capture/fallback implementation in all builds. The legacy global trace resolver and Cheryl::SignalHandlers target remain available; the demo needs only the selected engine/module targets. NDEBUG controls signal bootstrap installation, not explicit trace capture. The legacy ST_ON_SIGNALS definition exposes the sh declaration through core/logging.h; it does not change installation scope. See the signal procedure.

Independent consumers and header probes

Engine consumer links only Engine and exercises multiple translation units, events, workers and CPU resources. Its first-include probes include the neutral umbrellas and clipping contract and reject GL/GLFW header leakage. Standalone bootstrapping selects Engine only in a local scope.

Native GLFW consumer and OpenGL consumer link their actual module with first-include probes. They reference real implementation symbols without requiring a display at execution. CHERYL_BUILD_CONSUMER_TESTS=ON adds consumers for the selected root assembly.

The Engine-only consumer entry point is:

(
  set -e
  cd "$(git rev-parse --show-toplevel)"
  cmake -S projects/engine/tests/consumer -B build-consumer-engine -G Ninja \
    -DCMAKE_BUILD_TYPE=Release -DCHERYL_REPOSITORY_ROOT="$PWD"
  cmake --build build-consumer-engine --target consumer-cengine --parallel
  ./build-consumer-engine/cheryl-consumer
)

Standalone consumer entry points require CHERYL_REPOSITORY_ROOT=/path/to/cheryl-engine for shared helpers and bootstrapping. Engine, Native GLFW and OpenGL consumer bootstraps suppress both UI selections in a local variable scope, including inherited ON choices, without changing the enclosing configuration’s cache. Normal Native GLFW consumption selects no Cheryl OpenGL/GLAD dependency; the OpenGL consumer selects both integration owners. CMake 4 hosts may need CMAKE_POLICY_VERSION_MINIMUM=3.5 for pinned legacy dependency projects. Standalone module entry points and explicit null-platform selection are documented separately in the module guide. Architecture validation describes independent graph, consumer and runner checks and their coverage limits.

Optional output terminal

For Linux GNU/Clang consumers, explicitly link Cheryl::DebugTerminal::Linux (or the root’s selected Cheryl::DebugTerminal) to choose the process entry attachment. Plain Engine and command-line Startup do not select it. AUTO includes actual Debug only; ON permits a non-Debug consumer to display it with --debug-terminal. See the module guide for argument consumption, fallback and independent viewer lifetime.

The small Cheryl::DebugTerminal::Startup support target accepts an implementation callback; with no callback it calls the application unchanged. Native capture, viewer launch and process-wide shutdown live entirely in the selected module.

Standard headers in an existing build

An existing CMake/CLion profile can retain failed C++ compiler ABI discovery. Check whether generated CMakeCXXCompiler.cmake records CMAKE_CXX_ABI_COMPILED FALSE and empty implicit include directories. This can turn the X11 dependency’s /usr/include into an explicit -isystem argument; GCC can then fail to resolve CE::Math.h or stdlib.h through #include_next in C++ standard headers.

Refresh compiler discovery and regenerate the build files while preserving the profile/toolchain settings. CMake normally filters its detected implicit include directories from explicit compiler arguments. Reload CMake in CLion after repairing an existing build externally. Preserve intended profile/toolchain/cache settings when resetting generated discovery metadata or creating a fresh build directory.