From c162c07eb6d4b95a694a4505d48a9d18fd7e2951 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Wed, 14 Oct 2026 20:18:52 -0700 Subject: [PATCH 01/98] Fixes Linux Debug terminal compilation after namespace refactoring Uses the started control constant from the enclosing terminal Detail namespace instead of the removed TerminalDetail namespace. This restores Debug demo builds and explicit compilation of the terminal implementation in Release. Verifies the Debug demo and Release terminal implementation builds with 16 parallel jobs. --- .../modules/platform/debug-terminal-linux/src/main-wrap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/modules/platform/debug-terminal-linux/src/main-wrap.cpp b/projects/modules/platform/debug-terminal-linux/src/main-wrap.cpp index 562710da..225c39c9 100644 --- a/projects/modules/platform/debug-terminal-linux/src/main-wrap.cpp +++ b/projects/modules/platform/debug-terminal-linux/src/main-wrap.cpp @@ -161,7 +161,7 @@ namespace CE::Platform::Linux::DebugTerminal::Detail { stderr_owned_ = true; if (std::setvbuf(stdout, nullptr, _IOLBF, 0) != 0) throw std::runtime_error("Cannot configure live stdout capture"); - if (!send_control(control_.get(), TerminalDetail::started)) + if (!send_control(control_.get(), started)) native_failure("Start terminal output viewer"); reaper_ = std::jthread([this](const std::stop_token stop) { while (!stop.stop_requested()) { From 486078e252e68d48d5a9d202778594a57c88e45f Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 10:26:25 -0700 Subject: [PATCH 02/98] Fixes font collection loading with missing preferred files Skips explicit font files when opening fails and filesystem status confirms the path is absent, allowing remaining preferred fonts and the embedded fallback to load. Handles the absent-byte result in installed-family discovery as well. Preserves load failures for existing unreadable, corrupt or unsupported files, invalid face indices and filesystem status errors. Updates the public API contract comments for missing-file recovery. Validation: git diff --check and source review. Builds, test changes and execution, and prose documentation maintenance remain deferred under the repository work-mode instructions. --- .../engine/include/cheryl/text/font-collection.h | 8 ++++---- projects/engine/src/text/font-collection.cpp | 12 ++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/projects/engine/include/cheryl/text/font-collection.h b/projects/engine/include/cheryl/text/font-collection.h index 4134d22f..fca53ec2 100644 --- a/projects/engine/include/cheryl/text/font-collection.h +++ b/projects/engine/include/cheryl/text/font-collection.h @@ -16,7 +16,7 @@ namespace CE::Text { struct FontAccess; } - /** An explicit font file and zero-based collection face index. */ + /** An explicit font file and zero-based collection face index; missing files are skipped during loading. */ struct FontFile { std::filesystem::path path; std::uint32_t face_index{}; @@ -45,7 +45,7 @@ namespace CE::Text { /** Immutable font bytes and ordered scalable Unicode faces. CPU-only loading * snapshots application files and optional installed families, then appends an - * embedded DejaVu Sans fallback. Missing installed families are skipped; an + * embedded DejaVu Sans fallback. Missing files and installed families are skipped; an * unreadable/unsupported explicit file fails the whole load. No provider is used. */ class FontCollection { @@ -55,8 +55,8 @@ namespace CE::Text { public: /** Load an immutable font-byte snapshot without a provider or graphics context. - * Invalid selections, explicit file failures and unsupported faces reject - * the load; missing installed families are skipped. Allocation failures propagate. + * Invalid selections, unreadable files and unsupported faces reject the load; + * missing files and installed families are skipped. Allocation failures propagate. * @param selection Ordered application fonts and optional system-discovery policy, * borrowed only for this call. * @return Owned ordered faces ending in the embedded fallback, shared by copies. diff --git a/projects/engine/src/text/font-collection.cpp b/projects/engine/src/text/font-collection.cpp index 451c663c..24bb6e4c 100644 --- a/projects/engine/src/text/font-collection.cpp +++ b/projects/engine/src/text/font-collection.cpp @@ -37,8 +37,12 @@ namespace CE::Text { FontBytes read_font(const std::filesystem::path& path) { std::ifstream input(path, std::ios::binary | std::ios::ate); - if (!input) + if (!input) { + std::error_code error; + if (!std::filesystem::exists(path, error) && !error) + return {}; throw ::CE::Exceptions::runtime_exception(CE_HERE, "Unable to open font '" + path.string() + "'"); + } const auto length = static_cast(input.tellg()); if (length <= 0 || static_cast(length) > static_cast(std::numeric_limits::max()) || static_cast(length) > std::numeric_limits::max() || @@ -96,9 +100,11 @@ namespace CE::Text { FreeTypeFace first(nullptr, FT_Done_Face); try { bytes = read_font(file); + if (!bytes) + continue; first = open_face(library, {bytes, 0}); } catch (const ::CE::Exceptions::runtime_exception&) { - continue; // Installed-font discovery is best effort; explicit files below are strict. + continue; // Installed-font discovery is best effort. } const auto count = first->num_faces; if (count <= 0 || static_cast(count) > std::numeric_limits::max()) @@ -193,6 +199,8 @@ namespace CE::Text { if (const auto* file = std::get_if(&source)) { const auto path = file->path.lexically_normal(); const Detail::FontFaceData data{read_font(path), file->face_index}; + if (!data.bytes) + continue; const auto face = Detail::open_face(library.get(), data); append_face(*result, data, face_info(face.get(), path, file->face_index)); } else { From bb5701fa7ab782ef2d069c2cfd78581d58e0bbeb Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 10:41:52 -0700 Subject: [PATCH 03/98] Adds name-based font selection with application asset precedence Accepts plain family names and asset filename aliases in preferred sources. Discovers fonts recursively below configured application asset roots, defaults to assets, and resolves application matches before automatically discovered system families. Retains explicit file/face and system-only requests, ordered deduplication and the embedded fallback. Uses the selected asset root in first-game and demo, updates demo family options, and documents name matching, root configuration, source precedence and missing-file recovery in API comments and existing guides. Adds isolated regressions for named asset/system resolution, nested filename aliases, collision precedence, ordering, missing sources, invalid names and corrupt candidates. Reconciles the prior missing-file exception expectation. Validation: source review, changed-range clang-format checks, git diff --check and 78 local documentation path checks. No builds or tests were run. --- docs/assets/file-and-font-discovery.md | 25 ++- docs/assets/text-layout.md | 77 ++++++--- docs/getting-started/first-game.md | 20 ++- projects/apps/demo/README.md | 29 ++-- projects/apps/demo/src/demo-game.cpp | 1 + projects/apps/demo/src/demo-options.cpp | 5 +- projects/apps/first-game/src/first-game.cpp | 3 +- .../include/cheryl/text/font-collection.h | 34 ++-- projects/engine/src/text/font-collection.cpp | 65 +++++--- .../engine/tests/unit/src/font-selection.cpp | 151 +++++++++++++++++- 10 files changed, 332 insertions(+), 78 deletions(-) diff --git a/docs/assets/file-and-font-discovery.md b/docs/assets/file-and-font-discovery.md index 345a60a8..c5e86e58 100644 --- a/docs/assets/file-and-font-discovery.md +++ b/docs/assets/file-and-font-discovery.md @@ -2,8 +2,9 @@ File indexing, font discovery and asset loading have separate policies. The [asset loader](asset-loading.md) constructs a fresh `CE::Files::FileIndex` for each -preparation. Applications choose fonts explicitly -before loading them through their resource provider. +preparation. Unicode font selection and text preparation run on the CPU; text +upload uses the resource provider separately. Legacy fonts retain their +provider-owned loading path. Automatic graphics preparation registers JSON, WAV and shader paths in a fresh private `CE::Assets::Loading::FileRegistry`; only exact `graphics-manifests.json` indexes select documents. @@ -44,6 +45,26 @@ index and visited-directory markers, including the root recorded before traversa reconstruct the index to retry that root. There is no atomic discovery snapshot or automatic recovery from changed files. +## Font names and application assets + +[`CE::Text::FontSelection::preferred`](text-layout.md#font-selection) accepts an +ordered list of names such as `{"Arial", "Noto Sans", "DejaVuSans.ttf"}`. +Family names come from font metadata. Filename aliases, including their extension, +also select application fonts, regardless of where they are nested under an asset +root. Both forms compare ASCII letters without case. Application discovery scans +`asset_directories` recursively, defaulting to `{"assets"}` relative to the working +directory. Games using a different asset root set that list once; preferred names +contain no directories. + +Names resolve against application fonts before system fonts, so a matching asset +font overrides an installed copy. The engine discovers system roots automatically; +system files match family names rather than filename aliases. Missing names and +unusable discovery candidates are skipped. `FontFile` remains available for direct +path/face selection and `SystemFontFamily` for an explicitly system-only choice. +The [text guide](text-layout.md#font-selection) defines ordering, style ranking, +failure behavior and the embedded fallback. The lower-level functions below return +candidate paths; their filename heuristic is separate from this name resolution. + ## System-font candidates [fonts-system.h](../../projects/engine/include/cheryl/core/resources/fileio/fonts-system.h) diff --git a/docs/assets/text-layout.md b/docs/assets/text-layout.md index 5b293d81..3d093a5a 100644 --- a/docs/assets/text-layout.md +++ b/docs/assets/text-layout.md @@ -25,31 +25,66 @@ text retain their separate roles. ## Font selection `CE::Text::FontCollection::load(selection)` creates an immutable CPU snapshot of font -bytes. `FontSelection::preferred` interleaves application `CE::Text::FontFile` paths/collection -face indices and `CE::Text::SystemFontFamily` names in the application's desired order. Face -indices select ordinary collection faces, with a variable font's default coordinates; -named variation instances are not selected through that index. File -entries are strict: missing, unreadable, corrupt, nonscalable or non-Unicode faces -fail loading. An unavailable installed family is skipped. Nothing is published on -failure, and earlier font collections remain usable. Allocation failures propagate. +bytes. Put font names in `FontSelection::preferred`, in the order the game should +try them. Each name searches application fonts first, then installed system fonts; +the engine discovers system directories without application setup. For example: + +```cpp +CE::Text::FontSelection font_selection; +font_selection.preferred = {"Arial", "Noto Sans", "DejaVuSans.ttf"}; +font_selection.automatic_system_fonts = false; +auto fonts = CE::Text::FontCollection::load(font_selection); +``` + +Here `Arial` and `Noto Sans` are family names read from font metadata, independent +of their filenames. `DejaVuSans.ttf` is a filename alias for an application font; +it can be nested anywhere under the application asset roots. Names compare ASCII +letters without case. System fonts resolve by family name; filename aliases apply +only to application fonts. A missing name is skipped, so other preferences still +load. Empty names and names containing NUL are invalid. The embedded fallback +remains available even if none of these names resolve. + +`asset_directories` defaults to `{"assets"}`, relative to the process's working +directory. Font discovery recursively scans these roots for `.ttf`, `.otf`, `.ttc` +and `.otc` files, without requiring a `fonts/` subdirectory or graphics manifest. +If the game already has a selected asset root, set it once: + +```cpp +font_selection.asset_directories = {asset_root}; +``` + +Set that before calling `load()`. An empty list disables application-font discovery. +When both an application font and a system font match a name, the application +version wins. Within the chosen inventory, selection first prefers faces that are +neither bold-flagged nor heavier than Medium (weight 500), then upright faces, then +the declared OpenType weight closest to Regular (400). When numeric weight metadata +is absent, the bold flag supplies a 700/400 estimate. Sorted path/face order breaks +ties, including repeated application filenames. Discovery skips missing, unreadable +and unsupported candidates; allocation failures propagate. + +For explicit control, the same `preferred` list also accepts +`CE::Text::FontFile{path, face_index}` and `CE::Text::SystemFontFamily{name}`. +`FontFile` loads that path directly; a relative path uses the working directory, +not the asset roots. Its zero-based face index selects an ordinary collection face +with a variable font's default coordinates, rather than a named variation instance. +Missing explicit files are skipped; existing unreadable, corrupt, nonscalable or +non-Unicode faces fail the whole load. `SystemFontFamily` restricts lookup to the +installed inventory, bypassing application overrides; unavailable families are +skipped. Nothing is published on failure, and earlier font collections remain usable. System discovery uses the existing -[root traversal](file-and-font-discovery.md#system-font-candidates), but family -selection inspects font metadata rather than guessing from filenames. Family names -compare ASCII letters without case. Selection first prefers faces that are neither -bold-flagged nor heavier than Medium (weight 500), then upright faces, then the -declared OpenType weight closest to Regular (400). When numeric weight metadata -is absent, the bold flag supplies a 700/400 weight estimate. Sorted path/face order -breaks ties. Discovery is best effort. Set `system_directories` -to an explicit root list for application-controlled inventory, or an empty list for -no installed-font search; unset uses the system roots. Keep environment/files stable -during loading. A snapshot no longer needs those files afterward. +[root traversal](file-and-font-discovery.md#system-font-candidates). Leave +`system_directories` unset for automatic system roots; an explicit list replaces +them, and an empty list disables installed-font search. Keep environment/files +stable during loading. A snapshot no longer needs those files afterward. With `automatic_system_fonts`, append available Arial, Segoe UI, Helvetica, Noto Sans, -DejaVu Sans and Liberation Sans families in that order, skipping a family if its -best available face is bold-flagged or heavier than 500. Explicit preferred file -and family sources remain unrestricted by this automatic-weight limit; explicit -families use the same ranking but can select a heavy face when no lighter one exists. +DejaVu Sans and Liberation Sans families in that order, preferring application +fonts over system fonts and skipping a family if its selected face is bold-flagged +or heavier than 500. A heavy asset override therefore skips that automatic family +even when a lighter system copy exists. Disabling it leaves explicitly preferred +names, files and families active. Preferred sources remain unrestricted by this automatic-weight +limit; name/family lookup can select a heavy face when no lighter one exists. These preferences are not guaranteed to exist across operating systems or regions. Repeated path/face entries are deduplicated. The terminal fallback is always an [embedded, licensed DejaVu Sans](../../assets/fonts/README.md) face independent of the diff --git a/docs/getting-started/first-game.md b/docs/getting-started/first-game.md index 179224a3..097b1b1f 100644 --- a/docs/getting-started/first-game.md +++ b/docs/getting-started/first-game.md @@ -225,7 +225,7 @@ that backend has been destroyed. See the ## 6. Select a font and prepare the score label Font selection chooses immutable font bytes and faces. It does not draw text or -upload a glyph atlas. Select a preferred installed family, lay out the message, +upload a glyph atlas. Select a preferred font name, lay out the message, prepare its pixels and geometry, then upload that complete value: ```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp @@ -235,11 +235,19 @@ prepare its pixels and geometry, then upload that complete value: :dedent: 8 ``` -{cpp:class}`CE::Text::FontCollection` tries the preferred `DejaVu Sans` family -and appends the embedded fallback. `automatic_system_fonts=false` disables the -additional automatic family choices; the explicitly preferred installed family -is still considered. A missing family is skipped, so the score remains usable -without that system installation. +{cpp:class}`CE::Text::FontCollection` resolves the preferred `DejaVu Sans` family +from the selected application asset root first, then the system's font directories, +and appends the embedded fallback. The family name comes from the font's metadata; +it need not match its filename. A filename such as `DejaVuSans.ttf` can also select +an application font nested anywhere below `selection.asset_directories`. + +The engine discovers installed fonts automatically. `automatic_system_fonts=false` +disables additional automatic family choices; explicitly preferred names still +resolve against both application and system fonts. Missing names are skipped, so +the score remains usable without that font installation. If an asset font and an +installed font share the requested name, the asset version wins. See the +[font-selection example](../assets/text-layout.md#font-selection) for several +ordered preferences and direct file selection. {cpp:func}`CE::Text::layout_text` shapes the UTF-8 message using the selected font data. {cpp:func}`CE::Text::Rendering::prepare` creates owned CPU atlas pages and diff --git a/projects/apps/demo/README.md b/projects/apps/demo/README.md index cdac7ae8..b44181c0 100644 --- a/projects/apps/demo/README.md +++ b/projects/apps/demo/README.md @@ -50,8 +50,9 @@ adapters for the toolkit-free F2 text probe. Reload CMake after changing cached selections; the [build guide](../../../docs/development/building.md) owns option defaults and dependency settings. -The builtin HUD selects optional system/application fonts and always retains an -embedded fallback; it can start without an installed system font. RmlUi keeps its +The builtin HUD selects fonts by name from the asset root, then system directories, +and always retains an embedded fallback; it can start without an installed system +font. An asset font overrides a system font with the same name. RmlUi keeps its separate file-based service, using a selected file or the checked-in DejaVu Sans fallback. TGUI uses its own embedded default font. Shaders and the small RmlUi proof document/images are checked in under the asset root. The tile/sprite showcase attempts to load three optional package images @@ -120,9 +121,9 @@ The demo's [option helper](src/demo-options.cpp) registers these Application opt | `--max-updates=N` | Stops after N updates; zero leaves the run interactive. | | `--unicode-text` | Shows accented Latin, Cyrillic, mixed/pure RTL, missing-glyph and wrapping examples in the builtin HUD. | | `--text-runs` | Prepares positioned geometry for builtin HUD/camera text and sample labels, then submits one draw per contiguous atlas-page run. Toolkit text is unchanged. | -| `--builtin-font` | Disables automatic installed-family selection. Without explicit font preferences, the builtin HUD uses only its embedded fallback. | -| `--font=/path/to/font.ttf` | Appends an application file to the ordered preferred font sources; repeat as needed. Invalid files fail startup. | -| `--font-family=NAME` | Appends an installed family to the same preferred order. Quote names containing spaces; unavailable families are skipped. | +| `--builtin-font` | Disables automatic family selection. Without explicit font preferences, the builtin HUD uses only its embedded fallback. | +| `--font=/path/to/font.ttf` | Appends an explicit file to the ordered preferred font sources; repeat as needed. Missing files are skipped; existing invalid or unreadable files fail startup. | +| `--font-family=NAME` | Appends a name to the same preferred order, resolving asset fonts before installed families. Asset filenames including their extension also work. Quote names containing spaces; missing names are skipped. | | `--text-direction=auto`, `ltr` or `rtl` | Sets builtin paragraph direction. Latin/numbers keep their natural run direction; explicit RTL aligns HUD lines to the width's right edge. | See [simulation timing](../../../docs/runtime/simulation-timing.md) and @@ -374,17 +375,19 @@ in automatic mode, so its order need not change between automatic and explicit R ./build/testing-native-linux/demo --unicode-text assets ./build/testing-native-linux/demo --unicode-text --concurrent assets ./build/testing-native-linux/demo --unicode-text --builtin-font --font=assets/fonts/DejaVuSans.ttf assets - env XDG_DATA_HOME="$PWD/assets" ./build/testing-native-linux/demo \ - --unicode-text --builtin-font '--font-family=DejaVu Sans' assets + ./build/testing-native-linux/demo --unicode-text --builtin-font '--font-family=DejaVu Sans' assets + ./build/testing-native-linux/demo --unicode-text --builtin-font --font-family=DejaVuSans.ttf assets ) ``` -The `XDG_DATA_HOME` launch supplies a controlled family-discovery root containing -the repository font. Installed DejaVu Sans candidates can precede it; either is a -valid explicit family selection. Automatic discovery can use only the embedded -fallback when preferred families are absent. Successful fallback alone does not -prove discovery; controlled CPU cases establish its metadata and ordering. Numeric -font-weight limits still need the [dedicated fixtures](../../../docs/planning/short-term/unicode-text.md#progress). +The family-name and filename launches select the repository font from the asset +root, including nested font directories. The asset copy takes precedence over +installed DejaVu Sans. No system-directory or environment setup is required. +Automatic discovery can use only the embedded fallback when preferred families +are absent. Successful fallback alone does not prove discovery; inspect +`FontCollection::faces()` to distinguish a discovered file from the embedded face. +Numeric font-weight limits still need the +[dedicated fixtures](../../../docs/planning/short-term/unicode-text.md#progress). - Inspect English, French accents and `e`+combining acute, German umlauts/`ß`, and Russian glyphs. Accents stay with their base and glyph masks are upright, with diff --git a/projects/apps/demo/src/demo-game.cpp b/projects/apps/demo/src/demo-game.cpp index 5651843f..348a4152 100644 --- a/projects/apps/demo/src/demo-game.cpp +++ b/projects/apps/demo/src/demo-game.cpp @@ -162,6 +162,7 @@ class GameImpl : public CE::Game::AbstractGame { font_shader_ = std::move(materials.text); image_shader_ = std::move(materials.images); // Font bytes/layout are CPU values; initial uploads run on this platform owner. + font_selection_.asset_directories = {asset_root_}; fonts_.emplace(CE::Text::FontCollection::load(font_selection_)); text_workers_.emplace(engine_.make_worker_group({.max_concurrency = 1})); target_text_ = std::make_shared(CE::Text::Rendering::upload( diff --git a/projects/apps/demo/src/demo-options.cpp b/projects/apps/demo/src/demo-options.cpp index f9f32e12..91c912c3 100644 --- a/projects/apps/demo/src/demo-options.cpp +++ b/projects/apps/demo/src/demo-options.cpp @@ -36,9 +36,8 @@ void DemoOptions::register_with(CE::Application::Startup& startup) { ->trigger_on_parse(); options .add_option_function( - "--font-family", - [this](const std::string& family) { font_selection.preferred.push_back(CE::Text::SystemFontFamily{family}); }, - "Preferred system font family; repeat to add ordered fallbacks" + "--font-family", [this](const std::string& family) { font_selection.preferred.push_back(family); }, + "Preferred font family or asset filename; repeat to add ordered fallbacks" ) ->trigger_on_parse(); options diff --git a/projects/apps/first-game/src/first-game.cpp b/projects/apps/first-game/src/first-game.cpp index 644b30f3..530e8df3 100644 --- a/projects/apps/first-game/src/first-game.cpp +++ b/projects/apps/first-game/src/first-game.cpp @@ -49,7 +49,8 @@ namespace FirstGame { // [score] CE::Text::FontSelection selection; - selection.preferred.emplace_back(CE::Text::SystemFontFamily{"DejaVu Sans"}); + selection.preferred = {"DejaVu Sans"}; + selection.asset_directories = {asset_root_}; selection.automatic_system_fonts = false; auto fonts = CE::Text::FontCollection::load(selection); auto layout = CE::Text::layout_text(fonts, "Score: 0", {.pixel_height = 24}); diff --git a/projects/engine/include/cheryl/text/font-collection.h b/projects/engine/include/cheryl/text/font-collection.h index fca53ec2..a929b9da 100644 --- a/projects/engine/include/cheryl/text/font-collection.h +++ b/projects/engine/include/cheryl/text/font-collection.h @@ -21,18 +21,27 @@ namespace CE::Text { std::filesystem::path path; std::uint32_t face_index{}; }; - /** An installed family requested by name; missing families are skipped during loading. */ + /** A system-only family request, bypassing application assets; missing families are skipped. */ struct SystemFontFamily { std::string name; }; - using FontSource = std::variant; + /** Plain strings resolve family names or asset filenames, preferring application assets over system fonts. + * FontFile selects a path/face directly; SystemFontFamily restricts a name to installed fonts. + */ + using FontSource = std::variant; - /** Ordered application choices and optional installed-font discovery. */ + /** Ordered font names and optional explicit sources, with application and installed-font discovery. */ struct FontSelection { - std::vector<::CE::Text::FontSource> preferred; ///< Files/families in application order. - bool automatic_system_fonts = true; ///< Append automatic installed-family choices. - /** Unset uses system roots; an empty vector disables installed-font discovery. */ + std::vector<::CE::Text::FontSource> preferred; ///< Names or explicit sources in application order; missing entries are skipped. + bool automatic_system_fonts = true; ///< Append automatic family choices, preferring application assets. + /** Unset resolves system roots automatically; an empty vector disables installed-font discovery. */ std::optional> system_directories; + /** Recursively search these application roots for family names and filename aliases (including extensions). + * Names compare ASCII letters without case; an application match wins over an installed match. + * Defaults to assets relative to the working directory; supply the game's asset root when different. + * An empty vector disables application discovery. FontFile paths are independent of these roots. + */ + std::vector asset_directories{"assets"}; }; struct FontFaceInfo { std::string family; @@ -44,9 +53,10 @@ namespace CE::Text { }; /** Immutable font bytes and ordered scalable Unicode faces. CPU-only loading - * snapshots application files and optional installed families, then appends an - * embedded DejaVu Sans fallback. Missing files and installed families are skipped; an - * unreadable/unsupported explicit file fails the whole load. No provider is used. + * resolves preferred names from application assets before installed fonts, snapshots + * the selected bytes, then appends an embedded DejaVu Sans fallback. Missing names + * and files are skipped; an unreadable/unsupported explicit file fails the whole + * load. No provider is used. */ class FontCollection { std::shared_ptr data_; @@ -55,9 +65,9 @@ namespace CE::Text { public: /** Load an immutable font-byte snapshot without a provider or graphics context. - * Invalid selections, unreadable files and unsupported faces reject the load; - * missing files and installed families are skipped. Allocation failures propagate. - * @param selection Ordered application fonts and optional system-discovery policy, + * Invalid names and unreadable/unsupported explicit files reject the load; + * missing names/files and unusable discovery candidates are skipped. Allocation failures propagate. + * @param selection Ordered font names/sources, application roots and optional system-discovery policy, * borrowed only for this call. * @return Owned ordered faces ending in the embedded fallback, shared by copies. */ diff --git a/projects/engine/src/text/font-collection.cpp b/projects/engine/src/text/font-collection.cpp index 24bb6e4c..6baf2757 100644 --- a/projects/engine/src/text/font-collection.cpp +++ b/projects/engine/src/text/font-collection.cpp @@ -89,13 +89,17 @@ namespace CE::Text { std::map discover_families( FT_Library library, const FontSelection& selection, - const std::vector& requested + const std::vector& requested, + const bool application_assets ) { std::map result; if (requested.empty()) return result; - const auto files = selection.system_directories ? find_system_fonts(*selection.system_directories) : find_system_fonts(); + const auto files = application_assets ? find_system_fonts(selection.asset_directories) + : selection.system_directories ? find_system_fonts(*selection.system_directories) + : find_system_fonts(); for (const auto& file : files) { + const auto filename = application_assets ? family_key(file.filename().string()) : std::string{}; FontBytes bytes; FreeTypeFace first(nullptr, FT_Done_Face); try { @@ -104,27 +108,32 @@ namespace CE::Text { continue; first = open_face(library, {bytes, 0}); } catch (const ::CE::Exceptions::runtime_exception&) { - continue; // Installed-font discovery is best effort. + continue; // Name-based discovery is best effort. } const auto count = first->num_faces; if (count <= 0 || static_cast(count) > std::numeric_limits::max()) continue; for (FT_Long index = 0; index < count; ++index) { FreeTypeFace face(nullptr, FT_Done_Face); + const auto face_index = static_cast(index); try { - face = index == 0 ? std::move(first) : open_face(library, {bytes, static_cast(index)}); + face = index == 0 ? std::move(first) : open_face(library, {bytes, face_index}); + const auto key = family_key(face->family_name ? face->family_name : ""); + const auto score = style_score(face.get()); + const auto retain = [&](const std::string& name) { + if (std::ranges::find(requested, name) == requested.end()) + return; + const auto old = result.find(name); + if (old != result.end() && old->second.style_score <= score) + return; // Equal styles keep sorted path/face order. + result.insert_or_assign(name, Candidate{{bytes, face_index}, face_info(face.get(), file, face_index), score}); + }; + retain(key); + if (application_assets && filename != key) + retain(filename); } catch (const ::CE::Exceptions::runtime_exception&) { continue; } - const auto key = family_key(face->family_name ? face->family_name : ""); - if (std::ranges::find(requested, key) == requested.end()) - continue; - const auto score = style_score(face.get()); - const auto old = result.find(key); - if (old != result.end() && old->second.style_score <= score) - continue; // Equal styles keep sorted path/face order. - result.insert_or_assign(key, Candidate{{bytes, static_cast(index)}, - face_info(face.get(), file, static_cast(index)), score}); } } return result; @@ -182,18 +191,35 @@ namespace CE::Text { FontCollection FontCollection::load(const FontSelection& selection) { auto library = Detail::open_freetype(); std::vector requested; + std::vector asset_requested; for (const auto& source : selection.preferred) { if (const auto* family = std::get_if(&source)) { if (family->name.empty() || family->name.find('\0') != std::string::npos) throw ::CE::Exceptions::invalid_args(CE_HERE, "An installed font family needs a nonempty name without NUL"); requested.push_back(family_key(family->name)); + } else if (const auto* name = std::get_if(&source)) { + if (name->empty() || name->find('\0') != std::string::npos) + throw ::CE::Exceptions::invalid_args(CE_HERE, "A preferred font needs a nonempty name without NUL"); + requested.push_back(family_key(*name)); + asset_requested.push_back(requested.back()); } } if (selection.automatic_system_fonts) { - for (const auto name : automatic_families) + for (const auto name : automatic_families) { requested.push_back(family_key(name)); + asset_requested.push_back(requested.back()); + } } - const auto installed = discover_families(library.get(), selection, requested); + const auto assets = discover_families(library.get(), selection, asset_requested, true); + const auto installed = discover_families(library.get(), selection, requested, false); + const auto named_candidate = [&](const std::string_view name) -> const Candidate* { + const auto key = family_key(name); + if (const auto entry = assets.find(key); entry != assets.end()) + return &entry->second; + if (const auto entry = installed.find(key); entry != installed.end()) + return &entry->second; + return nullptr; + }; auto result = std::make_shared(); for (const auto& source : selection.preferred) { if (const auto* file = std::get_if(&source)) { @@ -203,6 +229,9 @@ namespace CE::Text { continue; const auto face = Detail::open_face(library.get(), data); append_face(*result, data, face_info(face.get(), path, file->face_index)); + } else if (const auto* name = std::get_if(&source)) { + if (const auto* entry = named_candidate(*name)) + append_face(*result, entry->data, entry->info); } else { const auto entry = installed.find(family_key(std::get(source).name)); if (entry != installed.end()) @@ -211,9 +240,9 @@ namespace CE::Text { } if (selection.automatic_system_fonts) { for (const auto name : automatic_families) { - const auto entry = installed.find(family_key(name)); - if (entry != installed.end() && !entry->second.style_score.heavy) - append_face(*result, entry->second.data, entry->second.info); + const auto* entry = named_candidate(name); + if (entry && !entry->style_score.heavy) + append_face(*result, entry->data, entry->info); } } const auto embedded = Detail::builtin_font_bytes(); diff --git a/projects/engine/tests/unit/src/font-selection.cpp b/projects/engine/tests/unit/src/font-selection.cpp index d3101c17..bed32897 100644 --- a/projects/engine/tests/unit/src/font-selection.cpp +++ b/projects/engine/tests/unit/src/font-selection.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,7 @@ namespace { CE::Text::FontSelection isolated_selection() { CE::Text::FontSelection selection; selection.automatic_system_fonts = false; + selection.asset_directories.clear(); selection.system_directories = std::vector{}; return selection; } @@ -88,6 +90,92 @@ TEST(font_selection, families) { EXPECT_EQ(automatic.faces()[0].path, renamed); } +TEST(font_selection, names) { + FontDirectory assets; + FontDirectory system; + const auto nested = assets.path / "nested"; + ASSERT_TRUE(std::filesystem::create_directory(nested)); + const auto application = nested / "GameFont.TTF"; + const auto installed = system.path / "installed.ttf"; + std::filesystem::copy_file(fixture, application); + std::filesystem::copy_file(fixture, installed); + auto selection = isolated_selection(); + selection.asset_directories = {assets.path}; + selection.system_directories = std::vector{system.path}; + selection.preferred = {"Absent Cheryl Test Family", "dEjAvU sAnS", "gamefont.ttf", "Absent.ttf"}; + const auto fonts = CE::Text::FontCollection::load(selection); + ASSERT_EQ(fonts.faces().size(), 2u); + EXPECT_EQ(fonts.faces()[0].path, application); + EXPECT_EQ(fonts.faces()[0].family, "DejaVu Sans"); + EXPECT_TRUE(fonts.faces()[1].builtin); + + selection.preferred = {"GameFont"}; + const auto no_stem = CE::Text::FontCollection::load(selection); + ASSERT_EQ(no_stem.faces().size(), 1u); + EXPECT_TRUE(no_stem.faces()[0].builtin); + + selection.preferred = {CE::Text::SystemFontFamily{"dEjAvU sAnS"}}; + const auto system_only = CE::Text::FontCollection::load(selection); + ASSERT_EQ(system_only.faces().size(), 2u); + EXPECT_EQ(system_only.faces()[0].path, installed); +} + +TEST(font_selection, name_order) { + FontDirectory assets; + const auto first = assets.path / "First.ttf"; + const auto second = assets.path / "Second.ttf"; + std::filesystem::copy_file(fixture, first); + std::filesystem::copy_file(fixture, second); + auto selection = isolated_selection(); + selection.asset_directories = {assets.path}; + selection.preferred = {"Second.ttf", "Absent.ttf", "FIRST.TTF", "second.ttf"}; + const auto fonts = CE::Text::FontCollection::load(selection); + ASSERT_EQ(fonts.faces().size(), 3u); + EXPECT_EQ(fonts.faces()[0].path, second); + EXPECT_EQ(fonts.faces()[1].path, first); + EXPECT_TRUE(fonts.faces()[2].builtin); + + selection.preferred = {"DejaVu Sans"}; + const auto family = CE::Text::FontCollection::load(selection); + ASSERT_EQ(family.faces().size(), 2u); + EXPECT_EQ(family.faces()[0].path, first); +} + +TEST(font_selection, named_system) { + FontDirectory directory; + const auto installed = directory.path / "installed.ttf"; + std::filesystem::copy_file(fixture, installed); + auto selection = isolated_selection(); + selection.system_directories = std::vector{directory.path}; + selection.preferred = {"dEjAvU sAnS"}; + const auto fonts = CE::Text::FontCollection::load(selection); + ASSERT_EQ(fonts.faces().size(), 2u); + EXPECT_EQ(fonts.faces()[0].path, installed); + EXPECT_TRUE(fonts.faces()[1].builtin); + + selection.system_directories = std::vector{}; + const auto missing = CE::Text::FontCollection::load(selection); + ASSERT_EQ(missing.faces().size(), 1u); + EXPECT_TRUE(missing.faces()[0].builtin); +} + +TEST(font_selection, automatic_assets) { + FontDirectory assets; + FontDirectory system; + const auto application = assets.path / "application.ttf"; + const auto installed = system.path / "installed.ttf"; + std::filesystem::copy_file(fixture, application); + std::filesystem::copy_file(fixture, installed); + auto selection = isolated_selection(); + selection.asset_directories = {assets.path}; + selection.system_directories = std::vector{system.path}; + selection.automatic_system_fonts = true; + const auto fonts = CE::Text::FontCollection::load(selection); + ASSERT_EQ(fonts.faces().size(), 2u); + EXPECT_EQ(fonts.faces()[0].path, application); + EXPECT_TRUE(fonts.faces()[1].builtin); +} + TEST(font_selection, snapshot) { FontDirectory directory; const auto file = directory.path / "font.ttf"; @@ -101,14 +189,73 @@ TEST(font_selection, snapshot) { EXPECT_TRUE(fonts.covers(U'\u00e9')); } +TEST(font_selection, missing) { + FontDirectory directory; + const auto first = directory.path / "first.ttf"; + const auto second = directory.path / "second.ttf"; + std::filesystem::copy_file(fixture, first); + std::filesystem::copy_file(fixture, second); + auto selection = isolated_selection(); + selection.preferred = { + CE::Text::FontFile{directory.path / "absent-before.ttf"}, CE::Text::FontFile{first}, + CE::Text::FontFile{directory.path / "absent-between.ttf"}, CE::Text::FontFile{second}, + CE::Text::FontFile{directory.path / "absent-after.ttf"} + }; + const auto fonts = CE::Text::FontCollection::load(selection); + ASSERT_EQ(fonts.faces().size(), 3u); + EXPECT_EQ(fonts.faces()[0].path, first); + EXPECT_EQ(fonts.faces()[1].path, second); + EXPECT_TRUE(fonts.faces()[2].builtin); + + selection.preferred = { + CE::Text::FontFile{directory.path / "absent-before.ttf"}, CE::Text::FontFile{directory.path / "absent-after.ttf"} + }; + const auto fallback = CE::Text::FontCollection::load(selection); + ASSERT_EQ(fallback.faces().size(), 1u); + EXPECT_TRUE(fallback.faces()[0].builtin); +} + TEST(font_selection, invalid) { auto selection = isolated_selection(); selection.preferred = {CE::Text::FontFile{fixture, 9999}}; EXPECT_THROW(static_cast(CE::Text::FontCollection::load(selection)), CE::Exceptions::runtime_exception); - selection.preferred = {CE::Text::FontFile{fixture.parent_path() / "absent.ttf"}}; - EXPECT_THROW(static_cast(CE::Text::FontCollection::load(selection)), CE::Exceptions::runtime_exception); selection.preferred = {CE::Text::SystemFontFamily{""}}; EXPECT_THROW(static_cast(CE::Text::FontCollection::load(selection)), CE::Exceptions::invalid_args); selection.preferred = {CE::Text::SystemFontFamily{std::string{"DejaVu\0Sans", 11}}}; EXPECT_THROW(static_cast(CE::Text::FontCollection::load(selection)), CE::Exceptions::invalid_args); + selection.preferred = {""}; + EXPECT_THROW(static_cast(CE::Text::FontCollection::load(selection)), CE::Exceptions::invalid_args); + selection.preferred = {std::string{"DejaVu\0Sans", 11}}; + EXPECT_THROW(static_cast(CE::Text::FontCollection::load(selection)), CE::Exceptions::invalid_args); +} + +TEST(font_selection, corrupt) { + FontDirectory directory; + const auto corrupt = directory.path / "corrupt.ttf"; + { + std::ofstream output(corrupt, std::ios::binary); + output << "Not a font"; + ASSERT_TRUE(output.good()); + } + const auto empty = directory.path / "empty.ttf"; + { + std::ofstream output(empty, std::ios::binary); + ASSERT_TRUE(output.good()); + } + auto selection = isolated_selection(); + for (const auto& path : {corrupt, empty, directory.path}) { + selection.preferred = {CE::Text::FontFile{path}}; + EXPECT_THROW(static_cast(CE::Text::FontCollection::load(selection)), CE::Exceptions::runtime_exception); + } + + FontDirectory system; + const auto installed = system.path / "installed.ttf"; + std::filesystem::copy_file(fixture, installed); + selection.asset_directories = {directory.path}; + selection.system_directories = std::vector{system.path}; + selection.preferred = {"corrupt.ttf", "empty.ttf", "DejaVu Sans"}; + const auto discovered = CE::Text::FontCollection::load(selection); + ASSERT_EQ(discovered.faces().size(), 2u); + EXPECT_EQ(discovered.faces()[0].path, installed); + EXPECT_TRUE(discovered.faces()[1].builtin); } From ed42fe5e3eee6046a8a6368dd44070aeb2be3174 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 11:04:16 -0700 Subject: [PATCH 04/98] Adds public aggregate headers and a selected-component facade Provides cheryl/cheryl.h for the complete linked public API, cheryl/engine.h for neutral Engine contracts, and module/support groups guarded by target-propagated availability definitions. Every module owns include/cheryl/modules/.h; support owners expose matching cheryl/support umbrellas. Registers source components with Cheryl::Cheryl and exports the same facade in development SDKs. Preserves supplied assemblies, omits source-only terminal dependencies from SDK exports and build dependencies, and keeps granular Engine consumers neutral. Extends the module template and documentation extraction roots. Adds neutral and selected-component consumer probes, installed SDK wrapper checks and explicit CI selection for aggregate coverage. Validation: static include graph covers all 202 public headers, formatting and syntax checks pass, and compiler-free CMake configuration/export checks confirm cross-directory registration, SDK exclusion and supplied-facade preservation. No project builds or tests were run. --- .github/ci/run.py | 2 +- .github/ci/sdk-consumer/CMakeLists.txt | 13 +++- .github/ci/test_ci.py | 2 +- CMakeLists.txt | 1 + cmake/CherylModules.cmake | 1 + cmake/CherylPublicHeaders.cmake | 31 ++++++++ cmake/CherylSdk.cmake | 12 +++- cmake/CherylTargets.cmake | 2 + cmake/templates/Module.cmake.in | 1 + docs/generation/build.py | 1 + projects/engine/CMakeLists.txt | 1 + projects/engine/include/cheryl/cheryl.h | 10 +++ projects/engine/include/cheryl/engine.h | 25 +++++++ projects/engine/include/cheryl/modules.h | 29 ++++++++ projects/engine/include/cheryl/support.h | 25 +++++++ .../support/debug-terminal/CMakeLists.txt | 2 + .../include/cheryl/support/debug-terminal.h | 7 ++ .../engine/support/startup/CMakeLists.txt | 1 + .../startup/include/cheryl/support/startup.h | 7 ++ .../support/worker-topology/CMakeLists.txt | 4 ++ .../cheryl/support/worker-topology-hwloc.h | 7 ++ .../cheryl/support/worker-topology-json.h | 7 ++ .../tests/consumer/headers/CMakeLists.txt | 35 +++++++++ .../tests/consumer/headers/aggregate.cpp | 72 +++++++++++++++++++ .../modules/audio/miniaudio/CMakeLists.txt | 1 + .../include/cheryl/modules/miniaudio.h | 7 ++ .../tests/consumer/headers/CMakeLists.txt | 2 +- .../modules/graphics/opengl/CMakeLists.txt | 1 + .../opengl/include/cheryl/modules/opengl.h | 8 +++ .../opengl/support/startup/CMakeLists.txt | 1 + .../include/cheryl/support/opengl-startup.h | 7 ++ .../tests/consumer/headers/CMakeLists.txt | 1 + .../debug-terminal-linux/CMakeLists.txt | 5 ++ .../cheryl/modules/debug-terminal-linux.h | 7 ++ .../platform/native-glfw/CMakeLists.txt | 1 + .../include/cheryl/modules/native-glfw.h | 14 ++++ .../tests/consumer/headers/CMakeLists.txt | 1 + projects/modules/ui/rmlui/CMakeLists.txt | 1 + .../ui/rmlui/include/cheryl/modules/rmlui.h | 10 +++ .../tests/consumer/headers/CMakeLists.txt | 2 +- projects/modules/ui/tgui/CMakeLists.txt | 1 + .../ui/tgui/include/cheryl/modules/tgui.h | 11 +++ .../tests/consumer/headers/CMakeLists.txt | 2 +- 43 files changed, 373 insertions(+), 8 deletions(-) create mode 100644 cmake/CherylPublicHeaders.cmake create mode 100644 projects/engine/include/cheryl/cheryl.h create mode 100644 projects/engine/include/cheryl/engine.h create mode 100644 projects/engine/include/cheryl/modules.h create mode 100644 projects/engine/include/cheryl/support.h create mode 100644 projects/engine/support/debug-terminal/include/cheryl/support/debug-terminal.h create mode 100644 projects/engine/support/startup/include/cheryl/support/startup.h create mode 100644 projects/engine/support/worker-topology/include/cheryl/support/worker-topology-hwloc.h create mode 100644 projects/engine/support/worker-topology/include/cheryl/support/worker-topology-json.h create mode 100644 projects/engine/tests/consumer/headers/aggregate.cpp create mode 100644 projects/modules/audio/miniaudio/include/cheryl/modules/miniaudio.h create mode 100644 projects/modules/graphics/opengl/include/cheryl/modules/opengl.h create mode 100644 projects/modules/graphics/opengl/support/startup/include/cheryl/support/opengl-startup.h create mode 100644 projects/modules/platform/debug-terminal-linux/include/cheryl/modules/debug-terminal-linux.h create mode 100644 projects/modules/platform/native-glfw/include/cheryl/modules/native-glfw.h create mode 100644 projects/modules/ui/rmlui/include/cheryl/modules/rmlui.h create mode 100644 projects/modules/ui/tgui/include/cheryl/modules/tgui.h diff --git a/.github/ci/run.py b/.github/ci/run.py index 60a6bd77..02a64fe7 100644 --- a/.github/ci/run.py +++ b/.github/ci/run.py @@ -60,7 +60,7 @@ def main(arguments=None): "-DVCPKG_MANIFEST_NO_DEFAULT_FEATURES=ON", f"-DVCPKG_MANIFEST_FEATURES={';'.join(features)}", f"-DCMAKE_PROJECT_INCLUDE={source / '.github/ci/windows-hwloc.cmake'}"] - targets = ["consumer-cengine"] + targets = ["consumer-cengine", "consumer-headers-cheryl"] if args.sdk: targets.append("cheryl-sdk") consumers = ["cheryl-consumer"] diff --git a/.github/ci/sdk-consumer/CMakeLists.txt b/.github/ci/sdk-consumer/CMakeLists.txt index c1257e3f..5fdeaf4e 100644 --- a/.github/ci/sdk-consumer/CMakeLists.txt +++ b/.github/ci/sdk-consumer/CMakeLists.txt @@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.28) project(cheryl_sdk_consumer LANGUAGES CXX) find_package(Cheryl CONFIG REQUIRED) +if(NOT TARGET Cheryl::Cheryl) + message(FATAL_ERROR "The installed SDK must provide the Cheryl::Cheryl aggregate target") +endif() include("${CHERYL_REPOSITORY_ROOT}/cmake/CherylConsumers.cmake") enable_testing() @@ -42,17 +45,23 @@ if(TARGET Cheryl::Audio::Miniaudio) "${CHERYL_REPOSITORY_ROOT}/projects/modules/audio/miniaudio/tests/support") endif() -cheryl_add_header_probes(sdk-startup Cheryl::Startup HEADERS cheryl/core/engine/startup.h) +cheryl_add_header_probes(sdk-startup Cheryl::Startup NEUTRAL + HEADERS cheryl/core/engine/startup.h cheryl/support/startup.h) if(TARGET Cheryl::OpenGL::Startup) - cheryl_add_header_probes(sdk-opengl-startup Cheryl::OpenGL::Startup HEADERS cheryl/backends/opengl/startup.h) + cheryl_add_header_probes(sdk-opengl-startup Cheryl::OpenGL::Startup + HEADERS cheryl/backends/opengl/startup.h cheryl/support/opengl-startup.h) endif() add_executable(sdk-worker "${CHERYL_REPOSITORY_ROOT}/projects/engine/support/worker-topology/tests/consumer.cpp") target_link_libraries(sdk-worker PRIVATE Cheryl::Engine) if(TARGET Cheryl::WorkerTopology::Hwloc) + cheryl_add_header_probes(sdk-worker-hwloc Cheryl::WorkerTopology::Hwloc NEUTRAL + HEADERS cheryl/support/worker-topology-hwloc.h) target_link_libraries(sdk-worker PRIVATE Cheryl::WorkerTopology::Hwloc) target_compile_definitions(sdk-worker PRIVATE CHERYL_CONSUMER_WORKER_HWLOC=1) endif() if(TARGET Cheryl::WorkerTopology::Json) + cheryl_add_header_probes(sdk-worker-json Cheryl::WorkerTopology::Json NEUTRAL + HEADERS cheryl/support/worker-topology-json.h) target_link_libraries(sdk-worker PRIVATE Cheryl::WorkerTopology::Json) target_compile_definitions(sdk-worker PRIVATE CHERYL_CONSUMER_WORKER_JSON=1) endif() diff --git a/.github/ci/test_ci.py b/.github/ci/test_ci.py index ba359ce9..c0060ae2 100644 --- a/.github/ci/test_ci.py +++ b/.github/ci/test_ci.py @@ -81,7 +81,7 @@ def test_consumer(self): self.assertFalse(any(command[0] == "ctest" for command in self.commands)) self.assertIn("-DCHERYL_BUILD_TESTS=OFF", self.commands[0]) self.assertEqual(self.commands[1][self.commands[1].index("--target") + 1:self.commands[1].index("--parallel")], - ["consumer-cengine"]) + ["consumer-cengine", "consumer-headers-cheryl"]) def test_windows(self): with patch.object(ci.sys, "platform", "win32"): diff --git a/CMakeLists.txt b/CMakeLists.txt index ed59da74..b0f7a613 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ cheryl_configure_root_options() include(cmake/CherylTargets.cmake) include(cmake/CherylOutputs.cmake) include(cmake/CherylLinkage.cmake) +include(cmake/CherylPublicHeaders.cmake) set(CHERYL_REPOSITORY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}") set(CHERYL_BUILD_ROOT "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/cmake/CherylModules.cmake b/cmake/CherylModules.cmake index 1fb2f955..4792046f 100644 --- a/cmake/CherylModules.cmake +++ b/cmake/CherylModules.cmake @@ -2,6 +2,7 @@ include("${CMAKE_CURRENT_LIST_DIR}/CherylVersions.cmake") include("${CMAKE_CURRENT_LIST_DIR}/CherylOptions.cmake") include("${CMAKE_CURRENT_LIST_DIR}/CherylOutputs.cmake") include("${CMAKE_CURRENT_LIST_DIR}/CherylLinkage.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/CherylPublicHeaders.cmake") include_guard(GLOBAL) # A module reuses supplied targets or bootstraps one explicitly selected checkout. diff --git a/cmake/CherylPublicHeaders.cmake b/cmake/CherylPublicHeaders.cmake new file mode 100644 index 00000000..f07af860 --- /dev/null +++ b/cmake/CherylPublicHeaders.cmake @@ -0,0 +1,31 @@ +include_guard(GLOBAL) + +# Keep the umbrella's feature set aligned with target usage requirements in both +# source assemblies and installed SDKs. Engine itself remains dependency-neutral. +function(cheryl_register_public_component target) + cmake_parse_arguments(component "BUILD_ONLY" "FEATURE" "" ${ARGN}) + if(component_FEATURE) + target_compile_definitions(${target} INTERFACE "${component_FEATURE}=1") + endif() + if(NOT TARGET Cheryl::Cheryl) + add_library(${TARGET_LIB_CHERYL} INTERFACE) + add_library(Cheryl::Cheryl ALIAS ${TARGET_LIB_CHERYL}) + set_property(TARGET ${TARGET_LIB_CHERYL} PROPERTY CHERYL_OWNED_ASSEMBLY TRUE) + target_link_libraries(${TARGET_LIB_CHERYL} INTERFACE Cheryl::Engine) + endif() + get_target_property(assembly Cheryl::Cheryl ALIASED_TARGET) + if(NOT assembly) + set(assembly Cheryl::Cheryl) + endif() + get_target_property(owned_assembly ${assembly} CHERYL_OWNED_ASSEMBLY) + if(NOT owned_assembly) + return() # Supplied assemblies retain their original component selection. + endif() + if(NOT target STREQUAL "${TARGET_LIB_ENGINE}") + if(component_BUILD_ONLY) + target_link_libraries(${assembly} INTERFACE "$") + else() + target_link_libraries(${assembly} INTERFACE ${target}) + endif() + endif() +endfunction() diff --git a/cmake/CherylSdk.cmake b/cmake/CherylSdk.cmake index e616d69c..946858ee 100644 --- a/cmake/CherylSdk.cmake +++ b/cmake/CherylSdk.cmake @@ -49,6 +49,11 @@ function(cheryl_sdk_target target exported_name) get_target_property(links ${target} INTERFACE_LINK_LIBRARIES) if(links) set(installed_links "${links}") + if(target STREQUAL "${TARGET_LIB_CHERYL}") + # The SDK wrapper below defers nested expressions to the consumer; + # remove source-only assembly members before forming that wrapper. + list(FILTER installed_links EXCLUDE REGEX "^\\$/dependencies" installed_links "${installed_links}") @@ -81,7 +86,11 @@ function(cheryl_sdk_target target exported_name) LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT CherylSDK RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT CherylSDK OBJECTS DESTINATION ${CMAKE_INSTALL_LIBDIR}/objects COMPONENT CherylSDK) - set_property(GLOBAL APPEND PROPERTY CHERYL_SDK_TARGETS ${target}) + if(NOT target STREQUAL "${TARGET_LIB_CHERYL}") + # Distributed libraries are listed individually; the source facade also + # links diagnostics that are deliberately outside the SDK build. + set_property(GLOBAL APPEND PROPERTY CHERYL_SDK_TARGETS ${target}) + endif() endfunction() function(cheryl_sdk_headers directory) @@ -91,6 +100,7 @@ function(cheryl_sdk_headers directory) endfunction() cheryl_sdk_target(${TARGET_LIB_ENGINE} Engine) +cheryl_sdk_target(${TARGET_LIB_CHERYL} Cheryl) cheryl_sdk_target(${TARGET_LIB_ENGINE_S_LOGGING_CONFIG} LoggingConfig) get_target_property(logging_definitions ${TARGET_LIB_ENGINE_S_LOGGING_CONFIG} INTERFACE_COMPILE_DEFINITIONS) if(CHERYL_LOG_PROFILE STREQUAL "auto") diff --git a/cmake/CherylTargets.cmake b/cmake/CherylTargets.cmake index 907feeed..249d5608 100644 --- a/cmake/CherylTargets.cmake +++ b/cmake/CherylTargets.cmake @@ -1,6 +1,7 @@ # Reload declarations into the caller's scope, including standalone entry points. # Engine +set(TARGET_LIB_CHERYL cheryl_assembly) set(TARGET_LIB_ENGINE cengine) # Engine support @@ -78,6 +79,7 @@ set(TARGET_TEST_CONSUMER_MODULE_UI_RMLUI consumer-module-ui-rmlui) set(TARGET_TEST_CONSUMER_MODULE_AUDIO_MINIAUDIO consumer-module-audio-miniaudio) # Header probes +set(TARGET_TEST_CONSUMER_CHERYL_HEADERS consumer-headers-cheryl) set(TARGET_TEST_CONSUMER_ENGINE_HEADERS consumer-headers-cengine) set(TARGET_TEST_CONSUMER_MODULE_NATIVE_GLFW_HEADERS consumer-module-headers-native-glfw) set(TARGET_TEST_CONSUMER_MODULE_OPENGL_HEADERS consumer-module-headers--opengl) diff --git a/cmake/templates/Module.cmake.in b/cmake/templates/Module.cmake.in index dff44030..115e7618 100644 --- a/cmake/templates/Module.cmake.in +++ b/cmake/templates/Module.cmake.in @@ -10,6 +10,7 @@ file(GLOB_RECURSE module_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") add_library(${TARGET_LIB_MODULE_@MODULE_ID@} STATIC ${module_sources}) add_library(Cheryl::@MODULE_ALIAS@ ALIAS ${TARGET_LIB_MODULE_@MODULE_ID@}) +cheryl_register_public_component(${TARGET_LIB_MODULE_@MODULE_ID@} FEATURE CHERYL_HAS_@MODULE_ID@) set_target_properties(${TARGET_LIB_MODULE_@MODULE_ID@} PROPERTIES VERSION "${VERSION_PROJECT_MODULE_@MODULE_ID@}" OUTPUT_NAME "${OUTPUT_LIB_MODULE_@MODULE_ID@}" diff --git a/docs/generation/build.py b/docs/generation/build.py index b0ffc282..f72eba20 100644 --- a/docs/generation/build.py +++ b/docs/generation/build.py @@ -34,6 +34,7 @@ "projects/engine/support/worker-topology/include", "projects/engine/support/debug-terminal/include", "projects/modules/platform/native-glfw/include", + "projects/modules/platform/debug-terminal-linux/include", "projects/modules/graphics/opengl/include", "projects/modules/graphics/opengl/support/startup/include", "projects/modules/ui/tgui/include", diff --git a/projects/engine/CMakeLists.txt b/projects/engine/CMakeLists.txt index 844c323b..3cbea58a 100644 --- a/projects/engine/CMakeLists.txt +++ b/projects/engine/CMakeLists.txt @@ -8,6 +8,7 @@ add_subdirectory(support/signal-handlers) add_library(${TARGET_LIB_ENGINE} STATIC ${CHERYL_ENGINE_SOURCES}) add_library(Cheryl::Engine ALIAS ${TARGET_LIB_ENGINE}) +cheryl_register_public_component(${TARGET_LIB_ENGINE}) set_target_properties(${TARGET_LIB_ENGINE} PROPERTIES VERSION "${VERSION_PROJECT_ENGINE}" OUTPUT_NAME "${OUTPUT_LIB_ENGINE}" diff --git a/projects/engine/include/cheryl/cheryl.h b/projects/engine/include/cheryl/cheryl.h new file mode 100644 index 00000000..51f16d6b --- /dev/null +++ b/projects/engine/include/cheryl/cheryl.h @@ -0,0 +1,10 @@ +#pragma once + +/** @file + * @brief All public Engine headers and the module/support APIs enabled by linked Cheryl targets. + * Link Cheryl::Cheryl to include the complete configured assembly. + */ + +#include +#include +#include diff --git a/projects/engine/include/cheryl/engine.h b/projects/engine/include/cheryl/engine.h new file mode 100644 index 00000000..9b10ffee --- /dev/null +++ b/projects/engine/include/cheryl/engine.h @@ -0,0 +1,25 @@ +#pragma once + +/** @file + * @brief All backend-neutral public Engine headers. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/projects/engine/include/cheryl/modules.h b/projects/engine/include/cheryl/modules.h new file mode 100644 index 00000000..b6cc7213 --- /dev/null +++ b/projects/engine/include/cheryl/modules.h @@ -0,0 +1,29 @@ +#pragma once + +/** @file + * @brief Public module APIs selected by linked Cheryl targets. + */ + +#if defined(CHERYL_HAS_DEBUG_TERMINAL_LINUX) && CHERYL_HAS_DEBUG_TERMINAL_LINUX +#include +#endif + +#if defined(CHERYL_HAS_NATIVE_GLFW) && CHERYL_HAS_NATIVE_GLFW +#include +#endif + +#if defined(CHERYL_HAS_OPENGL) && CHERYL_HAS_OPENGL +#include +#endif + +#if defined(CHERYL_HAS_UI_TGUI) && CHERYL_HAS_UI_TGUI +#include +#endif + +#if defined(CHERYL_HAS_UI_RMLUI) && CHERYL_HAS_UI_RMLUI +#include +#endif + +#if defined(CHERYL_HAS_AUDIO_MINIAUDIO) && CHERYL_HAS_AUDIO_MINIAUDIO +#include +#endif diff --git a/projects/engine/include/cheryl/support.h b/projects/engine/include/cheryl/support.h new file mode 100644 index 00000000..bc2af809 --- /dev/null +++ b/projects/engine/include/cheryl/support.h @@ -0,0 +1,25 @@ +#pragma once + +/** @file + * @brief Public support APIs selected by linked Cheryl targets. + */ + +#if defined(CHERYL_HAS_SUPPORT_STARTUP) && CHERYL_HAS_SUPPORT_STARTUP +#include +#endif + +#if defined(CHERYL_HAS_SUPPORT_DEBUG_TERMINAL) && CHERYL_HAS_SUPPORT_DEBUG_TERMINAL +#include +#endif + +#if defined(CHERYL_HAS_SUPPORT_WORKER_TOPOLOGY_HWLOC) && CHERYL_HAS_SUPPORT_WORKER_TOPOLOGY_HWLOC +#include +#endif + +#if defined(CHERYL_HAS_SUPPORT_WORKER_TOPOLOGY_JSON) && CHERYL_HAS_SUPPORT_WORKER_TOPOLOGY_JSON +#include +#endif + +#if defined(CHERYL_HAS_SUPPORT_OPENGL_STARTUP) && CHERYL_HAS_SUPPORT_OPENGL_STARTUP +#include +#endif diff --git a/projects/engine/support/debug-terminal/CMakeLists.txt b/projects/engine/support/debug-terminal/CMakeLists.txt index f2d6b677..4f478b35 100644 --- a/projects/engine/support/debug-terminal/CMakeLists.txt +++ b/projects/engine/support/debug-terminal/CMakeLists.txt @@ -4,6 +4,8 @@ endif() add_library(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} STATIC EXCLUDE_FROM_ALL src/startup.cpp src/arguments.cpp) add_library(Cheryl::DebugTerminal::Startup ALIAS ${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL}) +cheryl_register_public_component(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} + FEATURE CHERYL_HAS_SUPPORT_DEBUG_TERMINAL BUILD_ONLY) set_target_properties(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} PROPERTIES VERSION "${VERSION_PROJECT_ENGINE}" OUTPUT_NAME "${OUTPUT_LIB_ENGINE_S_DEBUG_TERMINAL}" diff --git a/projects/engine/support/debug-terminal/include/cheryl/support/debug-terminal.h b/projects/engine/support/debug-terminal/include/cheryl/support/debug-terminal.h new file mode 100644 index 00000000..4c6d1515 --- /dev/null +++ b/projects/engine/support/debug-terminal/include/cheryl/support/debug-terminal.h @@ -0,0 +1,7 @@ +#pragma once + +/** @file + * @brief Optional debug-terminal startup callback API. + */ + +#include diff --git a/projects/engine/support/startup/CMakeLists.txt b/projects/engine/support/startup/CMakeLists.txt index 78676368..9d15c518 100644 --- a/projects/engine/support/startup/CMakeLists.txt +++ b/projects/engine/support/startup/CMakeLists.txt @@ -8,6 +8,7 @@ endif() add_library(${TARGET_LIB_ENGINE_S_STARTUP} STATIC src/startup.cpp) add_library(Cheryl::Startup ALIAS ${TARGET_LIB_ENGINE_S_STARTUP}) +cheryl_register_public_component(${TARGET_LIB_ENGINE_S_STARTUP} FEATURE CHERYL_HAS_SUPPORT_STARTUP) set_target_properties(${TARGET_LIB_ENGINE_S_STARTUP} PROPERTIES VERSION "${VERSION_PROJECT_ENGINE}" OUTPUT_NAME "${OUTPUT_LIB_ENGINE_S_STARTUP}" diff --git a/projects/engine/support/startup/include/cheryl/support/startup.h b/projects/engine/support/startup/include/cheryl/support/startup.h new file mode 100644 index 00000000..66555bbd --- /dev/null +++ b/projects/engine/support/startup/include/cheryl/support/startup.h @@ -0,0 +1,7 @@ +#pragma once + +/** @file + * @brief Common command-line startup and runtime configuration APIs. + */ + +#include diff --git a/projects/engine/support/worker-topology/CMakeLists.txt b/projects/engine/support/worker-topology/CMakeLists.txt index be0e9c84..b407c2ff 100644 --- a/projects/engine/support/worker-topology/CMakeLists.txt +++ b/projects/engine/support/worker-topology/CMakeLists.txt @@ -3,6 +3,8 @@ if(CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC AND NOT TARGET Cheryl::WorkerTopology::Hwl add_library(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC} STATIC src/hwloc.cpp) add_library(Cheryl::WorkerTopology::Hwloc ALIAS ${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC}) + cheryl_register_public_component(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC} + FEATURE CHERYL_HAS_SUPPORT_WORKER_TOPOLOGY_HWLOC) set_target_properties(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC} PROPERTIES VERSION "${VERSION_PROJECT_ENGINE}" OUTPUT_NAME "${OUTPUT_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC}" @@ -18,6 +20,8 @@ endif() if(CHERYL_BUILD_WORKER_TOPOLOGY_JSON AND NOT TARGET Cheryl::WorkerTopology::Json) add_library(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON} STATIC src/configuration.cpp) add_library(Cheryl::WorkerTopology::Json ALIAS ${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON}) + cheryl_register_public_component(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON} + FEATURE CHERYL_HAS_SUPPORT_WORKER_TOPOLOGY_JSON) set_target_properties(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON} PROPERTIES VERSION "${VERSION_PROJECT_ENGINE}" OUTPUT_NAME "${OUTPUT_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON}" diff --git a/projects/engine/support/worker-topology/include/cheryl/support/worker-topology-hwloc.h b/projects/engine/support/worker-topology/include/cheryl/support/worker-topology-hwloc.h new file mode 100644 index 00000000..da94be45 --- /dev/null +++ b/projects/engine/support/worker-topology/include/cheryl/support/worker-topology-hwloc.h @@ -0,0 +1,7 @@ +#pragma once + +/** @file + * @brief Optional hwloc topology provider factory. + */ + +#include diff --git a/projects/engine/support/worker-topology/include/cheryl/support/worker-topology-json.h b/projects/engine/support/worker-topology/include/cheryl/support/worker-topology-json.h new file mode 100644 index 00000000..12f78b09 --- /dev/null +++ b/projects/engine/support/worker-topology/include/cheryl/support/worker-topology-json.h @@ -0,0 +1,7 @@ +#pragma once + +/** @file + * @brief Optional JSON worker configuration loading APIs. + */ + +#include diff --git a/projects/engine/tests/consumer/headers/CMakeLists.txt b/projects/engine/tests/consumer/headers/CMakeLists.txt index 82151359..9c1b3af9 100644 --- a/projects/engine/tests/consumer/headers/CMakeLists.txt +++ b/projects/engine/tests/consumer/headers/CMakeLists.txt @@ -1,5 +1,9 @@ # Each translation unit includes its header first, with no umbrella/prefix header. set(consumer_headers + cheryl/engine.h + cheryl/cheryl.h + cheryl/modules.h + cheryl/support.h cheryl/core.h cheryl/core/controls.h cheryl/core/display.h @@ -33,3 +37,34 @@ set(consumer_headers cheryl/assets/types/2d/unicode-text.h cheryl/assets/resources/resource-provider.h) cheryl_add_header_probes(${TARGET_TEST_CONSUMER_ENGINE_HEADERS} ${LINKAGE_PRIVATE_TEST_CONSUMER_ENGINE} NEUTRAL HEADERS ${consumer_headers}) +target_sources(${TARGET_TEST_CONSUMER_ENGINE_HEADERS} PRIVATE aggregate.cpp) + +if(TARGET Cheryl::Cheryl) + add_library(${TARGET_TEST_CONSUMER_CHERYL_HEADERS} OBJECT aggregate.cpp) + target_link_libraries(${TARGET_TEST_CONSUMER_CHERYL_HEADERS} PRIVATE Cheryl::Cheryl) + set(aggregate_owners + "Cheryl::NativeGLFW|NATIVE_GLFW" + "Cheryl::DebugTerminal::Linux|DEBUG_TERMINAL_LINUX" + "Cheryl::OpenGL|OPENGL" + "Cheryl::UI::TGUI|UI_TGUI" + "Cheryl::UI::RmlUi|UI_RMLUI" + "Cheryl::Audio::Miniaudio|AUDIO_MINIAUDIO" + "Cheryl::Startup|SUPPORT_STARTUP" + "Cheryl::DebugTerminal::Startup|SUPPORT_DEBUG_TERMINAL" + "Cheryl::WorkerTopology::Hwloc|SUPPORT_WORKER_TOPOLOGY_HWLOC" + "Cheryl::WorkerTopology::Json|SUPPORT_WORKER_TOPOLOGY_JSON" + "Cheryl::OpenGL::Startup|SUPPORT_OPENGL_STARTUP") + foreach(owner IN LISTS aggregate_owners) + string(REPLACE "|" ";" contract "${owner}") + list(GET contract 0 owner_target) + list(GET contract 1 feature) + if(TARGET ${owner_target}) + target_compile_definitions(${TARGET_TEST_CONSUMER_CHERYL_HEADERS} PRIVATE "CHERYL_CONSUMER_${feature}=1") + endif() + endforeach() + if(TARGET Cheryl::DebugTerminal::Linux) + cheryl_add_header_probes(${TARGET_TEST_CONSUMER_CHERYL_HEADERS}-debug-terminal Cheryl::DebugTerminal::Linux + HEADERS cheryl/modules/debug-terminal-linux.h) + add_dependencies(${TARGET_TEST_CONSUMER_CHERYL_HEADERS} ${TARGET_TEST_CONSUMER_CHERYL_HEADERS}-debug-terminal) + endif() +endif() diff --git a/projects/engine/tests/consumer/headers/aggregate.cpp b/projects/engine/tests/consumer/headers/aggregate.cpp new file mode 100644 index 00000000..89d54b3e --- /dev/null +++ b/projects/engine/tests/consumer/headers/aggregate.cpp @@ -0,0 +1,72 @@ +#include + +#include +#include +#include +#include + +static_assert(sizeof(CE::Workers::Pool) > 0); +static_assert(sizeof(CE::Text::FontSelection) > 0); +static_assert(sizeof(CE::Audio::Clip) > 0); +static_assert(sizeof(CE::Rendering::Frame) > 0); + +#if defined(CHERYL_CONSUMER_NATIVE_GLFW) +static_assert(sizeof(CE::Platform::GLFW::DisplaySystem) > 0); +static_assert(sizeof(CE::Platform::GLFW::Window) > 0); +#if CHERYL_NATIVE_INPUT +static_assert(sizeof(CE::Platform::GLFW::InputSystem) > 0); +#endif +#endif + +#if defined(CHERYL_CONSUMER_DEBUG_TERMINAL_LINUX) +#if !defined(CHERYL_HAS_DEBUG_TERMINAL_LINUX) || !CHERYL_HAS_DEBUG_TERMINAL_LINUX +#error The aggregate target must expose the selected Linux debug terminal module. +#endif +#endif + +#if defined(CHERYL_CONSUMER_OPENGL) +static_assert(sizeof(CE::Rendering::OpenGL::Renderer) > 0); +static_assert(sizeof(CE::Rendering::OpenGL::ResourceProvider) > 0); +#elif defined(GL_VERSION_3_3) || defined(GLFW_VERSION_MAJOR) +#error Consumers without the OpenGL module must not inherit graphics SDK headers. +#endif + +#if defined(CHERYL_CONSUMER_UI_TGUI) +static_assert(sizeof(CE::UI::TGUI::Scene) > 0); +static_assert(sizeof(CE::UI::TGUI::Session) > 0); +#endif + +#if defined(CHERYL_CONSUMER_UI_RMLUI) +static_assert(sizeof(CE::UI::RmlUi::Scene) > 0); +static_assert(sizeof(CE::UI::RmlUi::Session) > 0); +#endif + +#if defined(CHERYL_CONSUMER_AUDIO_MINIAUDIO) +static_assert(sizeof(CE::Audio::Miniaudio::System) > 0); +#endif + +#if defined(miniaudio_h) +#error Public audio headers must not include the native audio SDK. +#endif + +#if defined(CHERYL_CONSUMER_SUPPORT_STARTUP) +static_assert(sizeof(CE::Application::Startup) > 0); +#endif + +#if defined(CHERYL_CONSUMER_SUPPORT_DEBUG_TERMINAL) +static_assert(std::is_same_v); +#endif + +#if defined(CHERYL_CONSUMER_SUPPORT_WORKER_TOPOLOGY_HWLOC) +static_assert(std::is_same_v>); +#endif + +#if defined(CHERYL_CONSUMER_SUPPORT_WORKER_TOPOLOGY_JSON) +static_assert(std::is_same_v); +#endif + +#if defined(CHERYL_CONSUMER_SUPPORT_OPENGL_STARTUP) +static_assert( + std::is_same_v())), CE::Application::Startup> +); +#endif diff --git a/projects/modules/audio/miniaudio/CMakeLists.txt b/projects/modules/audio/miniaudio/CMakeLists.txt index f4cf5ef5..66272f47 100644 --- a/projects/modules/audio/miniaudio/CMakeLists.txt +++ b/projects/modules/audio/miniaudio/CMakeLists.txt @@ -41,6 +41,7 @@ endif() file(GLOB_RECURSE cheryl_audio_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") add_library(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO} STATIC ${cheryl_audio_sources}) add_library(Cheryl::Audio::Miniaudio ALIAS ${TARGET_LIB_MODULE_AUDIO_MINIAUDIO}) +cheryl_register_public_component(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO} FEATURE CHERYL_HAS_AUDIO_MINIAUDIO) set_target_properties(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO} PROPERTIES VERSION "${VERSION_PROJECT_MODULE_AUDIO_MINIAUDIO}" OUTPUT_NAME "${OUTPUT_LIB_MODULE_AUDIO_MINIAUDIO}" diff --git a/projects/modules/audio/miniaudio/include/cheryl/modules/miniaudio.h b/projects/modules/audio/miniaudio/include/cheryl/modules/miniaudio.h new file mode 100644 index 00000000..255e5132 --- /dev/null +++ b/projects/modules/audio/miniaudio/include/cheryl/modules/miniaudio.h @@ -0,0 +1,7 @@ +#pragma once + +/** @file + * @brief Miniaudio decoding and playback APIs. + */ + +#include diff --git a/projects/modules/audio/miniaudio/tests/consumer/headers/CMakeLists.txt b/projects/modules/audio/miniaudio/tests/consumer/headers/CMakeLists.txt index 05cf5fb8..2a8fce33 100644 --- a/projects/modules/audio/miniaudio/tests/consumer/headers/CMakeLists.txt +++ b/projects/modules/audio/miniaudio/tests/consumer/headers/CMakeLists.txt @@ -1,3 +1,3 @@ cheryl_add_header_probes(${TARGET_TEST_CONSUMER_MODULE_AUDIO_MINIAUDIO_HEADERS} ${LINKAGE_PRIVATE_TEST_CONSUMER_MODULE_AUDIO_MINIAUDIO} NEUTRAL - HEADERS cheryl/backends/miniaudio.h) + HEADERS cheryl/modules/miniaudio.h cheryl/backends/miniaudio.h) diff --git a/projects/modules/graphics/opengl/CMakeLists.txt b/projects/modules/graphics/opengl/CMakeLists.txt index 30ddd628..c4ea4f2d 100644 --- a/projects/modules/graphics/opengl/CMakeLists.txt +++ b/projects/modules/graphics/opengl/CMakeLists.txt @@ -25,6 +25,7 @@ file(GLOB_RECURSE cheryl_opengl_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") add_library(${TARGET_LIB_MODULE_OPENGL} STATIC ${cheryl_opengl_sources}) add_library(Cheryl::OpenGL ALIAS ${TARGET_LIB_MODULE_OPENGL}) +cheryl_register_public_component(${TARGET_LIB_MODULE_OPENGL} FEATURE CHERYL_HAS_OPENGL) set_target_properties(${TARGET_LIB_MODULE_OPENGL} PROPERTIES VERSION "${VERSION_PROJECT_MODULE_OPENGL}" OUTPUT_NAME "${OUTPUT_LIB_MODULE_OPENGL}" diff --git a/projects/modules/graphics/opengl/include/cheryl/modules/opengl.h b/projects/modules/graphics/opengl/include/cheryl/modules/opengl.h new file mode 100644 index 00000000..f827633c --- /dev/null +++ b/projects/modules/graphics/opengl/include/cheryl/modules/opengl.h @@ -0,0 +1,8 @@ +#pragma once + +/** @file + * @brief All public OpenGL backend APIs. + */ + +#include +#include diff --git a/projects/modules/graphics/opengl/support/startup/CMakeLists.txt b/projects/modules/graphics/opengl/support/startup/CMakeLists.txt index adbf086d..e3efafa5 100644 --- a/projects/modules/graphics/opengl/support/startup/CMakeLists.txt +++ b/projects/modules/graphics/opengl/support/startup/CMakeLists.txt @@ -8,6 +8,7 @@ endif() add_library(${TARGET_LIB_MODULE_OPENGL_S_STARTUP} STATIC src/startup.cpp) add_library(Cheryl::OpenGL::Startup ALIAS ${TARGET_LIB_MODULE_OPENGL_S_STARTUP}) +cheryl_register_public_component(${TARGET_LIB_MODULE_OPENGL_S_STARTUP} FEATURE CHERYL_HAS_SUPPORT_OPENGL_STARTUP) set_target_properties(${TARGET_LIB_MODULE_OPENGL_S_STARTUP} PROPERTIES VERSION "${VERSION_PROJECT_MODULE_OPENGL}" OUTPUT_NAME "${OUTPUT_LIB_MODULE_OPENGL_S_STARTUP}" diff --git a/projects/modules/graphics/opengl/support/startup/include/cheryl/support/opengl-startup.h b/projects/modules/graphics/opengl/support/startup/include/cheryl/support/opengl-startup.h new file mode 100644 index 00000000..ce8524e4 --- /dev/null +++ b/projects/modules/graphics/opengl/support/startup/include/cheryl/support/opengl-startup.h @@ -0,0 +1,7 @@ +#pragma once + +/** @file + * @brief GLFW/OpenGL command-line startup factories. + */ + +#include diff --git a/projects/modules/graphics/opengl/tests/consumer/headers/CMakeLists.txt b/projects/modules/graphics/opengl/tests/consumer/headers/CMakeLists.txt index a6fe1920..6cc5a5aa 100644 --- a/projects/modules/graphics/opengl/tests/consumer/headers/CMakeLists.txt +++ b/projects/modules/graphics/opengl/tests/consumer/headers/CMakeLists.txt @@ -1,5 +1,6 @@ cheryl_add_header_probes(${TARGET_TEST_CONSUMER_MODULE_OPENGL_HEADERS} ${LINKAGE_PRIVATE_TEST_CONSUMER_MODULE_OPENGL} HEADERS + cheryl/modules/opengl.h cheryl/backends/opengl.h cheryl/backends/opengl/context.h cheryl/backends/opengl/gl.h diff --git a/projects/modules/platform/debug-terminal-linux/CMakeLists.txt b/projects/modules/platform/debug-terminal-linux/CMakeLists.txt index f5823294..d6f4fd92 100644 --- a/projects/modules/platform/debug-terminal-linux/CMakeLists.txt +++ b/projects/modules/platform/debug-terminal-linux/CMakeLists.txt @@ -22,6 +22,11 @@ endif() add_library(${TARGET_LIB_MODULE_DEBUG_TERMINAL_LINUX} INTERFACE) add_library(Cheryl::DebugTerminal::Linux ALIAS ${TARGET_LIB_MODULE_DEBUG_TERMINAL_LINUX}) +cheryl_register_public_component(${TARGET_LIB_MODULE_DEBUG_TERMINAL_LINUX} + FEATURE CHERYL_HAS_DEBUG_TERMINAL_LINUX BUILD_ONLY) +target_include_directories(${TARGET_LIB_MODULE_DEBUG_TERMINAL_LINUX} INTERFACE + "$" + "$") target_compile_definitions(${TARGET_LIB_MODULE_DEBUG_TERMINAL_LINUX} INTERFACE "CHERYL_DEBUG_TERMINAL_AVAILABLE=$") diff --git a/projects/modules/platform/debug-terminal-linux/include/cheryl/modules/debug-terminal-linux.h b/projects/modules/platform/debug-terminal-linux/include/cheryl/modules/debug-terminal-linux.h new file mode 100644 index 00000000..2c69e8b4 --- /dev/null +++ b/projects/modules/platform/debug-terminal-linux/include/cheryl/modules/debug-terminal-linux.h @@ -0,0 +1,7 @@ +#pragma once + +/** @file + * @brief Linux debug-terminal module entry integration. + * This module has no public C++ declarations. Link Cheryl::DebugTerminal::Linux; + * CHERYL_DEBUG_TERMINAL selects the configurations that install its entry hook. + */ diff --git a/projects/modules/platform/native-glfw/CMakeLists.txt b/projects/modules/platform/native-glfw/CMakeLists.txt index 7b46da6c..2bb80b66 100644 --- a/projects/modules/platform/native-glfw/CMakeLists.txt +++ b/projects/modules/platform/native-glfw/CMakeLists.txt @@ -85,6 +85,7 @@ endif() add_library(${TARGET_LIB_MODULE_NATIVE_GLFW} STATIC ${cheryl_native_sources}) add_library(Cheryl::NativeGLFW ALIAS ${TARGET_LIB_MODULE_NATIVE_GLFW}) +cheryl_register_public_component(${TARGET_LIB_MODULE_NATIVE_GLFW} FEATURE CHERYL_HAS_NATIVE_GLFW) set_target_properties(${TARGET_LIB_MODULE_NATIVE_GLFW} PROPERTIES VERSION "${VERSION_PROJECT_MODULE_NATIVE_GLFW}" OUTPUT_NAME "${OUTPUT_LIB_MODULE_NATIVE_GLFW}" diff --git a/projects/modules/platform/native-glfw/include/cheryl/modules/native-glfw.h b/projects/modules/platform/native-glfw/include/cheryl/modules/native-glfw.h new file mode 100644 index 00000000..805a4175 --- /dev/null +++ b/projects/modules/platform/native-glfw/include/cheryl/modules/native-glfw.h @@ -0,0 +1,14 @@ +#pragma once + +/** @file + * @brief Native GLFW window, display, diagnostics and enabled input APIs. + */ + +#include +#include + +#if defined(CHERYL_NATIVE_INPUT) && CHERYL_NATIVE_INPUT +#include +#include +#include +#endif diff --git a/projects/modules/platform/native-glfw/tests/consumer/headers/CMakeLists.txt b/projects/modules/platform/native-glfw/tests/consumer/headers/CMakeLists.txt index 9a5f0030..6b515159 100644 --- a/projects/modules/platform/native-glfw/tests/consumer/headers/CMakeLists.txt +++ b/projects/modules/platform/native-glfw/tests/consumer/headers/CMakeLists.txt @@ -1,5 +1,6 @@ cheryl_add_header_probes(${TARGET_TEST_CONSUMER_MODULE_NATIVE_GLFW_HEADERS} ${LINKAGE_PRIVATE_TEST_CONSUMER_MODULE_NATIVE_GLFW} HEADERS + cheryl/modules/native-glfw.h cheryl/backends/native-glfw.h cheryl/backends/native-glfw/diagnostics.h cheryl/core/display/window.h diff --git a/projects/modules/ui/rmlui/CMakeLists.txt b/projects/modules/ui/rmlui/CMakeLists.txt index b483cc40..49d65f2d 100644 --- a/projects/modules/ui/rmlui/CMakeLists.txt +++ b/projects/modules/ui/rmlui/CMakeLists.txt @@ -41,6 +41,7 @@ file(GLOB_RECURSE cheryl_rmlui_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") add_library(${TARGET_LIB_MODULE_UI_RMLUI} STATIC ${cheryl_rmlui_sources}) add_library(Cheryl::UI::RmlUi ALIAS ${TARGET_LIB_MODULE_UI_RMLUI}) +cheryl_register_public_component(${TARGET_LIB_MODULE_UI_RMLUI} FEATURE CHERYL_HAS_UI_RMLUI) set_target_properties(${TARGET_LIB_MODULE_UI_RMLUI} PROPERTIES VERSION "${VERSION_PROJECT_MODULE_UI_RMLUI}" OUTPUT_NAME "${OUTPUT_LIB_MODULE_UI_RMLUI}" diff --git a/projects/modules/ui/rmlui/include/cheryl/modules/rmlui.h b/projects/modules/ui/rmlui/include/cheryl/modules/rmlui.h new file mode 100644 index 00000000..16ef49f6 --- /dev/null +++ b/projects/modules/ui/rmlui/include/cheryl/modules/rmlui.h @@ -0,0 +1,10 @@ +#pragma once + +/** @file + * @brief RmlUi input, recording, scene upload and session APIs. + */ + +#include +#include +#include +#include diff --git a/projects/modules/ui/rmlui/tests/consumer/headers/CMakeLists.txt b/projects/modules/ui/rmlui/tests/consumer/headers/CMakeLists.txt index a56aed91..b2083af8 100644 --- a/projects/modules/ui/rmlui/tests/consumer/headers/CMakeLists.txt +++ b/projects/modules/ui/rmlui/tests/consumer/headers/CMakeLists.txt @@ -1,2 +1,2 @@ cheryl_add_header_probes(${TARGET_TEST_CONSUMER_MODULE_UI_RMLUI_HEADERS} ${LINKAGE_PRIVATE_TEST_CONSUMER_MODULE_UI_RMLUI} NEUTRAL - HEADERS cheryl/ui/rmlui/input.h cheryl/ui/rmlui/rendering.h cheryl/ui/rmlui/scene.h cheryl/ui/rmlui/session.h) + HEADERS cheryl/modules/rmlui.h cheryl/ui/rmlui/input.h cheryl/ui/rmlui/rendering.h cheryl/ui/rmlui/scene.h cheryl/ui/rmlui/session.h) diff --git a/projects/modules/ui/tgui/CMakeLists.txt b/projects/modules/ui/tgui/CMakeLists.txt index dce8247b..80854b8f 100644 --- a/projects/modules/ui/tgui/CMakeLists.txt +++ b/projects/modules/ui/tgui/CMakeLists.txt @@ -26,6 +26,7 @@ file(GLOB_RECURSE cheryl_tgui_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") add_library(${TARGET_LIB_MODULE_UI_TGUI} STATIC ${cheryl_tgui_sources}) add_library(Cheryl::UI::TGUI ALIAS ${TARGET_LIB_MODULE_UI_TGUI}) +cheryl_register_public_component(${TARGET_LIB_MODULE_UI_TGUI} FEATURE CHERYL_HAS_UI_TGUI) set_target_properties(${TARGET_LIB_MODULE_UI_TGUI} PROPERTIES VERSION "${VERSION_PROJECT_MODULE_UI_TGUI}" OUTPUT_NAME "${OUTPUT_LIB_MODULE_UI_TGUI}" diff --git a/projects/modules/ui/tgui/include/cheryl/modules/tgui.h b/projects/modules/ui/tgui/include/cheryl/modules/tgui.h new file mode 100644 index 00000000..f15750c0 --- /dev/null +++ b/projects/modules/ui/tgui/include/cheryl/modules/tgui.h @@ -0,0 +1,11 @@ +#pragma once + +/** @file + * @brief TGUI input, layout, recording, scene upload and session APIs. + */ + +#include +#include +#include +#include +#include diff --git a/projects/modules/ui/tgui/tests/consumer/headers/CMakeLists.txt b/projects/modules/ui/tgui/tests/consumer/headers/CMakeLists.txt index e0ed5f29..2f51268f 100644 --- a/projects/modules/ui/tgui/tests/consumer/headers/CMakeLists.txt +++ b/projects/modules/ui/tgui/tests/consumer/headers/CMakeLists.txt @@ -1,2 +1,2 @@ cheryl_add_header_probes(${TARGET_TEST_CONSUMER_MODULE_UI_TGUI_HEADERS} ${LINKAGE_PRIVATE_TEST_CONSUMER_MODULE_UI_TGUI} NEUTRAL - HEADERS cheryl/ui/tgui/input.h cheryl/ui/tgui/layout.h cheryl/ui/tgui/rendering.h cheryl/ui/tgui/scene.h cheryl/ui/tgui/session.h) + HEADERS cheryl/modules/tgui.h cheryl/ui/tgui/input.h cheryl/ui/tgui/layout.h cheryl/ui/tgui/rendering.h cheryl/ui/tgui/scene.h cheryl/ui/tgui/session.h) From 9a958d0220c3139e51deb7c0b5d508848853cc3e Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 11:04:16 -0700 Subject: [PATCH 05/98] Documents artifact builds and complete public API consumption Adds artifact-oriented commands for documentation generation, selected libraries, SDK installation with aggregated redistributable headers, and packaged SDK archives. Distinguishes library compilation from installation and packaging. Shows source and installed SDK applications linking Cheryl::Cheryl and including cheryl/cheryl.h, explains granular module/support umbrellas and IDE include discovery, and records owner-local umbrella conventions and source-only diagnostic behavior. Validation: local documentation paths, shell block syntax and git diff --check. Documentation generation was not run. --- docs/api/index.md | 5 ++ docs/development/building.md | 82 +++++++++++++++++++++--- docs/development/consuming-engine.md | 93 ++++++++++++++++++++++++---- docs/development/documentation.md | 27 +++++++- docs/development/modules.md | 49 ++++++++++++--- 5 files changed, 226 insertions(+), 30 deletions(-) diff --git a/docs/api/index.md b/docs/api/index.md index cd777728..9bbccdcf 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -36,6 +36,11 @@ portable facilities are still provided by `Cheryl::Engine`. ## Entry points +Include `` and link `Cheryl::Cheryl` for the complete selected +assembly. Individual module umbrellas live under `cheryl/modules/`, and support +umbrellas under `cheryl/support/`. The [consumer guide](../development/consuming-engine.md#dependencies-and-headers) +explains source-checkout and SDK target selection. + | Task | Entry point | Declaring header | | --- | --- | --- | | Configure application startup | {cpp:class}`CE::Application::Startup` | [startup.h](../../projects/engine/support/startup/include/cheryl/core/engine/startup.h) | diff --git a/docs/development/building.md b/docs/development/building.md index 58a59023..e0997218 100644 --- a/docs/development/building.md +++ b/docs/development/building.md @@ -6,6 +6,48 @@ Use CMake 3.28 or newer, Ninja and a C++23 compiler/standard library with link Cheryl targets instead of copying include and link settings. Prebuilt consumers use the [SDK import targets](consuming-engine.md#prebuilt-development-sdks). +## Choose the artifact + +Configuration selects available targets; an explicit build, install or packaging +command produces the artifact you need. A library build leaves headers in their +source directories. SDK installation collects them into one redistributable +`include/` tree alongside the compiled libraries and CMake import targets. + +| Artifact | Trigger | Result | +| --- | --- | --- | +| HTML manual and C++ reference | Build `docs` in the docs-only project below, or enable `CHERYL_BUILD_DOCS` in an engine build. | `docs/generation/generated/html/index.html` and linked pages. | +| Engine library | Build `cengine`; see [engine-only configuration](#engine-only-configuration). | `libcheryl-engine.a` on Linux or `cheryl-engine.lib` on Windows in the build root, plus required dependency artifacts. | +| Selected SDK libraries | Enable `CHERYL_BUILD_SDK`, then build `cheryl-sdk`. | Engine, selected modules/support and their owned dependencies in the build tree. | +| Redistributable SDK directory | Build `cheryl-sdk`, then install the `CherylSDK` component. | One prefix containing `include/`, libraries, CMake package files and licenses. | +| SDK archive with manifest/checksum | Run `.github/ci/package.py` after building `cheryl-sdk`. | Linux `.tar.gz` or Windows `.zip`, `SDK-MANIFEST.json` and `SHA256SUMS`. | + +The [SDK commands](#development-sdk-builds) below cover building, installing and +archiving separately. Source consumers do not need to produce an SDK first; linking +`Cheryl::Cheryl` supplies the configured assembly's public include directories and +dependencies for ``. This interface target links the existing +libraries; there is no additional combined library archive. See +[consuming Cheryl](consuming-engine.md) for source and prebuilt integration. + +### Build documentation + +This uses no C/C++ compiler or engine dependencies. Run from any directory inside +the checkout; the subshell preserves your starting directory: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" + cmake -S docs/generation -B docs/generation/generated/cmake -G Ninja + cmake --build docs/generation/generated/cmake --target docs +) +``` + +Open `docs/generation/generated/html/index.html`. Python 3.11 or newer is required; +the generator selects compatible installed tools or prepares pinned tools in the +user cache. Initial tool setup may need network access. See +[documentation generation](documentation.md) for direct Python invocation, tool +selection and output options. The `docs` target prepares its tools automatically. + ## Configuration options Pass settings as `-DNAME=value`. These defaults apply to a fresh root configuration; @@ -201,7 +243,7 @@ with submodules at their recorded revisions for reproducible archives. The packa records the source commit and verifies recursive submodule HEADs against its gitlinks. Use a single-configuration Ninja build and select the modules/helpers to distribute. -On Linux, this produces the default Native GLFW/OpenGL/TGUI libraries and common +On Linux, this builds the default Native GLFW/OpenGL/TGUI libraries and common Startup support without building the demo or tests: ```sh @@ -215,16 +257,42 @@ Startup support without building the demo or tests: -DCHERYL_BUILD_DEBUG_TERMINAL_LINUX=OFF \ -DGAINPUT_ENABLE_HID=OFF -DGLFW_BUILD_WAYLAND=OFF cmake --build build/sdk --target cheryl-sdk --parallel +) +``` + +`cheryl-sdk` builds the selected libraries and their dependencies. It does not +copy headers, install a directory or create an archive. To collect the libraries +and redistributable headers into a local SDK prefix after that build: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" + cmake --install build/sdk --component CherylSDK --prefix "$PWD/build/sdk-prefix" +) +``` + +The prefix contains `include/`, `lib/`, `lib/cmake/Cheryl/` and `share/licenses/` +with the configuration above. Windows may also install runtime libraries under +`bin/`. The install command does not compile missing libraries; run the build step +first. Consumers use `find_package(Cheryl CONFIG REQUIRED)` with this prefix in +`CMAKE_PREFIX_PATH`; see [SDK consumption](consuming-engine.md#prebuilt-development-sdks). + +For a complete distributable archive, use Python 3.12 or newer: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" python3 .github/ci/package.py --build-dir build/sdk --output build/packages ) ``` -The packaging tools use Python 3.12 or newer. `package.py` installs only the -`CherylSDK` component, creates a Linux `.tar.gz` or Windows `.zip`, and writes -`SHA256SUMS` plus `SDK-MANIFEST.json`. A direct -`cmake --install build/sdk --component CherylSDK --prefix /absolute/sdk/prefix` -installs the library/header/CMake component; Windows archive production additionally -copies its native dependency prefix. Use `package.py` for the complete bundle. +`package.py` performs its own temporary install of only the `CherylSDK` component, +creates a Linux `.tar.gz` or Windows `.zip` under `build/packages/`, and writes +`SHA256SUMS` plus `SDK-MANIFEST.json` there. You can skip the direct install command +when you only need an archive. Windows archive production additionally copies its +native dependency prefix; use `package.py` for the complete Windows bundle. Archive names include the Cheryl version from [CherylVersions.cmake](../../cmake/CherylVersions.cmake), platform/distribution, architecture, compiler/version, configuration and commit ID. diff --git a/docs/development/consuming-engine.md b/docs/development/consuming-engine.md index ad9d0091..8416e40a 100644 --- a/docs/development/consuming-engine.md +++ b/docs/development/consuming-engine.md @@ -5,11 +5,52 @@ maintained application covering startup, controls, asset loading, and sprite/tex submission. This page describes the source and SDK consumption contracts. Applications can compose source targets from a checkout or link a prebuilt -[development SDK](#prebuilt-development-sdks). `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](modules.md) describes ownership, -standalone composition and tests. +[development SDK](#prebuilt-development-sdks). Link `Cheryl::Cheryl` to use the +complete configured assembly through one public include: + +```cpp +#include +``` + +That target combines Engine, configured modules and available support libraries. +Its include directories and feature definitions make the matching headers visible +to the compiler and the IDE. The same include works with source and SDK targets. +Individual targets remain available when the application needs a smaller dependency +set; `Cheryl::Engine` is the neutral library backed by build target `cengine` and +archive output name `cheryl-engine`. + +## Source checkout + +A relative path to the Cheryl checkout through `add_subdirectory` is supported. +For example, with the root's default Native GLFW/OpenGL/TGUI selections: + +```cmake +cmake_minimum_required(VERSION 3.28) +project(my_game LANGUAGES CXX) +set(CHERYL_BUILD_TESTS OFF) +set(CHERYL_BUILD_DEMO OFF) +set(CHERYL_BUILD_DEBUG_TERMINAL_LINUX OFF) +add_subdirectory(path/to/cheryl-engine cheryl) +add_executable(my_game main.cpp) +target_link_libraries(my_game PRIVATE Cheryl::Cheryl) +``` + +Replace `path/to/cheryl-engine` with the checkout path relative to your application's +`CMakeLists.txt`, or an absolute path. CMake builds the selected libraries and +propagates their public include paths and dependency settings. Reload the CMake +project in the IDE after changing selections so completion uses those settings. +An SDK install is optional for this workflow. The [SDK build](building.md#development-sdk-builds) +collects headers and compiled libraries into a separate prefix for consumers that +want a prebuilt distribution. + +The source facade also links the configured Linux Debug terminal and its entry +attachment when that module is enabled. The example disables it; use the granular +targets below when you want to choose support and diagnostic behavior individually. + +### Granular composition + +Link selected targets individually to control the application's dependency set. +This example selects neutral Engine alone: ```cmake set(CHERYL_BUILD_TESTS OFF) @@ -32,6 +73,7 @@ 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. +The [module guide](modules.md) describes ownership, standalone composition and tests. ## Prebuilt development SDKs @@ -44,8 +86,8 @@ 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. +target_link_libraries(my_game PRIVATE Cheryl::Cheryl) +# Or link Cheryl::Engine and individual module/support targets for a smaller set. ``` For a Linux Release SDK, configure the application's directory as follows: @@ -66,6 +108,10 @@ CMake import/version files and licenses. `.h`/`.hpp` and required `.inl`/`.ipp`/ 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. +Its `Cheryl::Cheryl` target selects the components distributed in that SDK; +`` exposes their public interfaces. Installed headers share the +`include/cheryl/` namespace, including the module and support umbrellas described +[below](#dependencies-and-headers). The [build guide](building.md#development-sdk-builds) describes production; `SDK-MANIFEST.json` records selected features, source/submodule revisions, OS, architecture, compiler/version, configuration, logging profile and native versions. @@ -184,10 +230,7 @@ With Native GLFW, OpenGL and native input enabled, this opens a window and stops when Escape is pressed. It submits no draws yet: ```cpp -#include -#include -#include -#include +#include #include class Game final : public CE::Game::AbstractGame { @@ -220,7 +263,8 @@ int main(int argc, char** argv) { } ``` -Link this example to `Cheryl::OpenGL::Startup`. Pass `--concurrent` to select the +Link this example to `Cheryl::Cheryl` with OpenGL selected, or to +`Cheryl::OpenGL::Startup` for granular composition. 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. @@ -245,6 +289,29 @@ entities, collision and mechanics belong to the game or its selected modules. ## Dependencies and headers +All public Cheryl includes share the `cheryl/` prefix in both source and SDK +consumption. The files remain with their owning projects in the checkout; target +usage requirements present those public roots together to the compiler and IDE. +SDK installation collects the roots into one physical `include/cheryl/` tree. + +| Include | Public scope | +| --- | --- | +| `` | Engine plus the modules and support selected by linked targets. | +| `` | All neutral Engine public headers. | +| `` | Modules selected by linked targets. | +| `` | Support libraries selected by linked targets. | +| `` and other `cheryl/modules/` umbrellas | One module's public headers. | +| `cheryl/support/` umbrellas | One support library's public headers. | + +The umbrellas list only public Cheryl headers. Some public headers transitively +use `internals/` helpers, so the SDK retains those files as implementation support; +applications do not include them directly. Including an umbrella does not enable +or link a module: CMake target selection supplies its +dependencies and the feature definitions used by the group umbrellas. Link +`Cheryl::Cheryl` for all configured components, or link granular public targets +and use the same umbrellas for that narrower selection. Additional includes such +as `` remain appropriate when directly using dependency APIs. + | Requirement | Target scope and reason | | --- | --- | | C++23, Threads, logging configuration | Engine public requirements used by templates/runtime consumers. | @@ -333,6 +400,8 @@ independent graph, consumer and runner checks and their coverage limits. For Linux GNU/Clang consumers, explicitly link `Cheryl::DebugTerminal::Linux` (or the root's selected `Cheryl::DebugTerminal`) to choose the process entry attachment. +The source `Cheryl::Cheryl` facade also includes it when configured; SDK facades +omit this source-only module and its Debug terminal support. 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](../../projects/modules/platform/debug-terminal-linux/README.md) diff --git a/docs/development/documentation.md b/docs/development/documentation.md index 3f15c78f..4de78874 100644 --- a/docs/development/documentation.md +++ b/docs/development/documentation.md @@ -5,6 +5,13 @@ selected public headers to XML; Breathe and Exhale turn that XML into Sphinx pag and MyST includes the Markdown guides. Documentation generation does not configure or compile the engine. +The generated artifact is an HTML site rooted at +`docs/generation/generated/html/index.html`, containing both the manual and API +reference. For an explicit CMake artifact target, use +[`docs` in the docs-only project](#cmake-entry-points); for direct generation, use +the Python command below. Enabling `CHERYL_BUILD_DOCS` makes the same target +available in an engine build; documentation is never part of the default build. + ## Generate locally Use a Git checkout and Python 3.11 or newer with the standard `venv` module. You @@ -139,9 +146,23 @@ This writes `docs/generation/generated/html/index.html`. The optional `docs-tool target prepares tools; the `docs` target also selects them, so running `docs-tools` first is unnecessary. Neither target belongs to the default build. -For an engine build, configure `CHERYL_BUILD_DOCS=ON` and explicitly build `docs`. -The root project still has its usual compiler and dependency requirements; use the -[building guide](building.md) for that configuration. Both CMake entry points expose +To add documentation to an existing root engine build, reconfigure that same build +directory and explicitly select the target: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" + cmake -S . -B build/debug -G Ninja -DCHERYL_BUILD_DOCS=ON + cmake --build build/debug --target docs +) +``` + +Use your existing build directory in place of `build/debug`; its other cached +settings are retained. The root project still has its usual compiler and dependency +requirements; use the [building guide](building.md) for an initial engine +configuration. Building `docs` runs documentation generation without compiling the +engine. Both CMake entry points expose `CHERYL_DOCS_PYTHON`, `CHERYL_DOCS_CACHE_DIR` and `CHERYL_DOCS_FORCE_PINNED`: ```sh diff --git a/docs/development/modules.md b/docs/development/modules.md index c89425ce..645c4f39 100644 --- a/docs/development/modules.md +++ b/docs/development/modules.md @@ -78,7 +78,8 @@ platform/graphics selection. OpenGL requires Native GLFW; the demo also requires native input. Disable demo and every module option for Engine alone. On Linux GNU/Clang this includes `CHERYL_BUILD_DEBUG_TERMINAL_LINUX=OFF`. The root automatically links the selected terminal interface to its demo/shared -test entry point; external applications select it explicitly. Its configuration +test entry point and the source `Cheryl::Cheryl` facade. External applications can +also select it individually through `Cheryl::DebugTerminal::Linux`. Its configuration policy and standalone probes are in the [owner guide](../../projects/modules/platform/debug-terminal-linux/README.md). @@ -88,11 +89,30 @@ Applications link public targets, for example: target_link_libraries(game PRIVATE Cheryl::Engine Cheryl::NativeGLFW Cheryl::OpenGL) ``` -Linking Engine alone supplies neutral contracts, not concrete native/graphics -implementations. Targets propagate public include roots, C++23, logging policy and -dependency requirements; raw archive consumers must supply their complete link -closure. Engine umbrellas remain neutral. Include concrete classes explicitly -through their owner's headers, such as `cheryl/backends/native-glfw.h`. +Alternatively, link `Cheryl::Cheryl` to select the complete configured assembly, +including its available support libraries: + +```cmake +target_link_libraries(game PRIVATE Cheryl::Cheryl) +``` + +This facade is an interface target over the existing libraries. Its source assembly +includes configured diagnostic support; the installed SDK omits the source-only +Linux Debug terminal and its support attachment. + +Both source and installed SDK consumers can then include `` for +the public API of their linked assembly. `` stays neutral; +`` and `` expose the linked module and support +groups. Per-owner umbrellas live under `cheryl/modules/` and `cheryl/support/`, so +these APIs appear under the same `cheryl/` include prefix in IDE completion. +The [consumer guide](consuming-engine.md#dependencies-and-headers) describes the +include layout and source-versus-SDK workflows. + +Linking Engine alone supplies neutral contracts. Targets propagate public include +roots, C++23, logging policy, module feature definitions and dependency requirements; +raw archive consumers must supply their complete link closure. Headers alone do +not select a backend or add its libraries. Concrete classes remain available through +granular owner headers, such as `cheryl/backends/native-glfw.h`. `CHERYL_NATIVE_INPUT=OFF` omits Gainput and the default-input factory overload. `CHERYL_NATIVE_NULL_PLATFORM=ON` configures an owned GLFW without X11/Wayland; @@ -133,13 +153,21 @@ owner's `tests/consumer/` entry point. Prebuilt selected owners are also availab through the [installed SDK](consuming-engine.md#prebuilt-development-sdks), which uses the same public aliases and its producer's compiled configuration. +Adding a source module beside a supplied SDK leaves the SDK's `Cheryl::Cheryl` +selection unchanged. Link that source module's target explicitly alongside the +SDK facade; its public feature definition enables its umbrella in the same include. + ## Add a module Resolve the boundary criteria above before adding a target. Then: 1. Create an owner under `projects/modules///` with `include/`, `src/` and `tests/`. Keep public contracts under `include/cheryl/`; implementation - helpers and SDK details stay private where the API permits it. + helpers and SDK details stay private where the API permits it. Add one + owner-local `include/cheryl/modules/.h` umbrella that includes its public + contracts and excludes `internals/`. Support libraries use the corresponding + `include/cheryl/support/` location. Include the owner umbrella from its selected + group in `cheryl/modules.h` or `cheryl/support.h`. 2. Copy [Module.cmake.in](../../cmake/templates/Module.cmake.in) into the owner's `CMakeLists.txt`. Replace `MODULE_NAME`, the uppercase `MODULE_ID` and public `MODULE_ALIAS`. `cheryl_prepare_module()` reuses or bootstraps Engine; pass @@ -147,7 +175,12 @@ Resolve the boundary criteria above before adding a target. Then: 3. Declare shared metadata using the same ID in the files below. Add SDK discovery inside the selected owner, reusing compatible supplied targets before owned sources. Publish required header dependencies with `PUBLIC`; keep private - implementation dependencies `PRIVATE`. Do not rewrite supplied targets/cache. + implementation dependencies `PRIVATE`. Call + `cheryl_register_public_component(target FEATURE CHERYL_HAS_)` from + [CherylPublicHeaders.cmake](../../cmake/CherylPublicHeaders.cmake) to register + the owner's public feature definition and add its target to `Cheryl::Cheryl`. + Use that same feature definition to guard its group umbrella include. Do not + rewrite supplied targets/cache. 4. Declare a root selection with `cheryl_option(name description default_value)` in [CherylOptions.cmake](../../cmake/CherylOptions.cmake), and conditionally add the owner in the root [CMakeLists.txt](../../CMakeLists.txt). No other module From 05f53fbc80effe7557d0071318531ef5daedf489 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 11:17:02 -0700 Subject: [PATCH 06/98] Fixes HID-enabled SDK exports and dependency documentation discovery Exports the owned HIDAPI backend selected by Gainput together with its header target, normalized public headers and license files. Translates Gainput's nested HIDAPI link alias to the installed dependency target and recreates required libudev, libusb and Iconv dependencies in the SDK package config. Imported HIDAPI remains an external package requirement. Disables documentation for Cheryl-owned GLFW in a variable scope so ordinary source configuration does not probe Doxygen. Documents that source applications can leave SDK export disabled and records HID-enabled SDK dependency requirements without changing controller runtime behavior. Validation: compiler-free CMake SDK export generation for hidraw, libusb, imported HIDAPI and HID-disabled graphs; header/license component staging; package Python and documentation shell/path syntax; git diff --check. No engine builds or runtime tests were run. --- .github/ci/package.py | 3 +- cmake/CherylSdk.cmake | 45 ++++++++++++++++++- cmake/templates/CherylConfig.cmake.in | 14 +++++- docs/development/building.md | 22 ++++++--- docs/development/consuming-engine.md | 10 ++++- .../platform/native-glfw/CMakeLists.txt | 15 ++++--- 6 files changed, 92 insertions(+), 17 deletions(-) diff --git a/.github/ci/package.py b/.github/ci/package.py index 57f08237..562e6985 100644 --- a/.github/ci/package.py +++ b/.github/ci/package.py @@ -139,7 +139,8 @@ def bundle(source, build, output, native_prefix=None): metadata["runtime_directory"] = "dependencies/bin" else: metadata["system_dependencies"] = ["FreeType", "HarfBuzz", "ICU", "hwloc (when selected)", - "libdw and its dependencies", "X11/OpenGL (when selected)"] + "libdw and its dependencies", "X11/OpenGL (when selected)", + "libudev/libusb/Iconv (when required by packaged HIDAPI)"] check_contents(stage) text = json.dumps(metadata, indent=2, sort_keys=True) + "\n" (stage / "SDK-MANIFEST.json").write_text(text, encoding="utf-8") diff --git a/cmake/CherylSdk.cmake b/cmake/CherylSdk.cmake index 946858ee..66bf345a 100644 --- a/cmake/CherylSdk.cmake +++ b/cmake/CherylSdk.cmake @@ -61,13 +61,18 @@ function(cheryl_sdk_target target exported_name) if(target STREQUAL "${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC}") set(installed_links "Cheryl::Engine;$") endif() + if(exported_name STREQUAL "DependencyGainput" AND TARGET hidapi::hidapi AND NOT sdk_hidapi_external) + # Gainput nests this alias inside conditional link expressions, which + # the SDK wrapper otherwise defers without translating its namespace. + string(REPLACE "hidapi::hidapi" "Cheryl::DependencyHidapi" installed_links "${installed_links}") + endif() string(REPLACE "$" "$" installed_links "${installed_links}") set_property(TARGET ${target} PROPERTY INTERFACE_LINK_LIBRARIES "$;$") endif() set(public_headers) - if(target STREQUAL "spdlog") + if(target STREQUAL "spdlog" OR exported_name STREQUAL "DependencyHidapi") get_target_property(header_files ${target} PUBLIC_HEADER) get_target_property(target_source ${target} SOURCE_DIR) set(absolute_headers) @@ -79,7 +84,12 @@ function(cheryl_sdk_target target exported_name) endif() endforeach() set_property(TARGET ${target} PROPERTY PUBLIC_HEADER "${absolute_headers}") - set(public_headers PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/spdlog COMPONENT CherylSDK) + if(target STREQUAL "spdlog") + set(header_destination "${CMAKE_INSTALL_INCLUDEDIR}/spdlog") + else() + set(header_destination "${CMAKE_INSTALL_INCLUDEDIR}/hidapi") + endif() + set(public_headers PUBLIC_HEADER DESTINATION ${header_destination} COMPONENT CherylSDK) endif() install(TARGETS ${target} EXPORT CherylSdkTargets ${public_headers} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT CherylSDK @@ -151,6 +161,10 @@ foreach(provider IN ITEMS HWLOC JSON) cheryl_sdk_headers("${CHERYL_REPOSITORY_ROOT}/projects/engine/support/worker-topology/include") endif() endforeach() +set(sdk_hidapi_external OFF) +set(sdk_hidapi_libudev OFF) +set(sdk_hidapi_libusb OFF) +set(sdk_hidapi_iconv OFF) if(CHERYL_BUILD_NATIVE_GLFW) set_property(TARGET ${TARGET_LIB_MODULE_NATIVE_GLFW} APPEND PROPERTY EXPORT_PROPERTIES CHERYL_NATIVE_INPUT) cheryl_sdk_target(${TARGET_LIB_MODULE_NATIVE_GLFW} NativeGLFW) @@ -158,6 +172,33 @@ if(CHERYL_BUILD_NATIVE_GLFW) cheryl_sdk_headers("${CHERYL_REPOSITORY_ROOT}/projects/modules/platform/native-glfw/include") cheryl_sdk_headers("${CHERYL_REPOSITORY_ROOT}/extern/glfw/include") if(CHERYL_NATIVE_INPUT) + if(TARGET hidapi::hidapi) + get_target_property(hidapi_target hidapi::hidapi ALIASED_TARGET) + if(NOT hidapi_target) + set(hidapi_target hidapi::hidapi) + endif() + get_target_property(sdk_hidapi_external ${hidapi_target} IMPORTED) + if(NOT sdk_hidapi_external) + cheryl_sdk_target(${hidapi_target} DependencyHidapi) + cheryl_sdk_target(hidapi_include DependencyHidapiHeaders) + target_include_directories(hidapi_include INTERFACE + "$") + get_target_property(hidapi_links ${hidapi_target} INTERFACE_LINK_LIBRARIES) + if(hidapi_links MATCHES "PkgConfig::libudev") + set(sdk_hidapi_libudev ON) + endif() + if(hidapi_links MATCHES "PkgConfig::libusb") + set(sdk_hidapi_libusb ON) + endif() + if(hidapi_links MATCHES "Iconv::Iconv") + set(sdk_hidapi_iconv ON) + endif() + get_target_property(hidapi_source ${hidapi_target} SOURCE_DIR) + file(GLOB hidapi_licenses "${hidapi_source}/../LICENSE*") + install(FILES ${hidapi_licenses} DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/hidapi + COMPONENT CherylSDK) + endif() + endif() foreach(candidate IN LISTS LINKAGE_PUBLIC_LIB_MODULE_NATIVE_GLFW_INPUT_GAINPUT_CANDIDATES) if(TARGET ${candidate}) cheryl_sdk_target(${candidate} DependencyGainput) diff --git a/cmake/templates/CherylConfig.cmake.in b/cmake/templates/CherylConfig.cmake.in index 94d18f7b..c4890718 100644 --- a/cmake/templates/CherylConfig.cmake.in +++ b/cmake/templates/CherylConfig.cmake.in @@ -59,9 +59,21 @@ if(@CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC@ AND NOT TARGET CherylSdk::Hwloc) IMPORTED_LOCATION "${cheryl_sdk_hwloc_library}" INTERFACE_INCLUDE_DIRECTORIES "${cheryl_sdk_hwloc_include}") endif() -if(WIN32 AND @CHERYL_BUILD_NATIVE_GLFW@ AND @CHERYL_NATIVE_INPUT@) +if(@sdk_hidapi_external@) find_dependency(hidapi CONFIG) endif() +if(@sdk_hidapi_libudev@ OR @sdk_hidapi_libusb@) + find_dependency(PkgConfig) + if(@sdk_hidapi_libudev@ AND NOT TARGET PkgConfig::libudev) + pkg_check_modules(libudev REQUIRED IMPORTED_TARGET libudev) + endif() + if(@sdk_hidapi_libusb@ AND NOT TARGET PkgConfig::libusb) + pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0>=1.0.9) + endif() +endif() +if(@sdk_hidapi_iconv@) + find_dependency(Iconv) +endif() set(CMAKE_PREFIX_PATH "${cheryl_sdk_prefix_path}") include("${CMAKE_CURRENT_LIST_DIR}/CherylTargets.cmake") check_required_components(Cheryl) diff --git a/docs/development/building.md b/docs/development/building.md index e0997218..0a67e170 100644 --- a/docs/development/building.md +++ b/docs/development/building.md @@ -24,8 +24,10 @@ source directories. SDK installation collects them into one redistributable The [SDK commands](#development-sdk-builds) below cover building, installing and archiving separately. Source consumers do not need to produce an SDK first; linking `Cheryl::Cheryl` supplies the configured assembly's public include directories and -dependencies for ``. This interface target links the existing -libraries; there is no additional combined library archive. See +dependencies for ``. Leave `CHERYL_BUILD_SDK=OFF` when building an +application from source unless you also want to produce an SDK. This interface +target links the existing libraries; there is no additional combined library +archive. See [consuming Cheryl](consuming-engine.md) for source and prebuilt integration. ### Build documentation @@ -85,6 +87,8 @@ declared only when RmlUi is selected. A supplied GLFW target keeps its host's settings. Null-platform OpenGL still needs OpenGL development/link libraries and does not supply the windowed demo. +Cheryl disables documentation generation for its owned GLFW dependency, so ordinary +engine configuration does not probe Doxygen for GLFW's manual. Standalone modules default their local `CHERYL_BUILD_TESTS` to `OFF`; the [module guide](modules.md#standalone-modules) covers their composition. @@ -186,7 +190,7 @@ Package names vary by operating system. | OpenGL | OpenGL module | Headers/link libraries; native execution also needs a usable driver/display. | | Python and Jinja2 | GLAD generation | Jinja2 must be available in CMake's selected interpreter. | | FreeType | TGUI / RmlUi | Each toolkit uses its own font service. | -| libudev / libusb | Linux hidapi | Requirements depend on selected HID backends. | +| libudev / libusb / Iconv | Linux hidapi | Requirements depend on selected HID backends; SDK consumers need the selected backend's native libraries too. | | Konsole or xterm | Linux output terminal at runtime | A desktop is needed for display; neither is an Engine build dependency. | | libdw, libbfd, or libdwarf/libelf | Backward, optional | Improve trace source/symbol resolution. | @@ -237,7 +241,8 @@ CPU vendor is not a CMake selection. ## Development SDK builds SDK export is opt-in and requires Cheryl-owned builds of the pinned source -dependencies; imported dependency targets are rejected by this packaging path. +dependencies; imported dependency targets are rejected by this packaging path, +except an imported hidapi package that remains external to the SDK. Ordinary source composition can still reuse supplied targets. Use a clean checkout with submodules at their recorded revisions for reproducible archives. The packager records the source commit and verifies recursive submodule HEADs against its gitlinks. @@ -261,8 +266,13 @@ Startup support without building the demo or tests: ``` `cheryl-sdk` builds the selected libraries and their dependencies. It does not -copy headers, install a directory or create an archive. To collect the libraries -and redistributable headers into a local SDK prefix after that build: +copy headers, install a directory or create an archive. With native input and HID +enabled, SDK export includes the owned hidapi backend selected by Gainput, its +headers and licenses. Linux consumers still need its selected libudev/libusb and +Iconv dependencies; the package config discovers them. This packaging support does +not change the [Linux controller limitations](../../projects/modules/platform/native-glfw/README.md#controller-backend-limits). +To collect the libraries and redistributable headers into a local SDK prefix after +that build: ```sh ( diff --git a/docs/development/consuming-engine.md b/docs/development/consuming-engine.md index 8416e40a..9ca867ed 100644 --- a/docs/development/consuming-engine.md +++ b/docs/development/consuming-engine.md @@ -29,6 +29,7 @@ cmake_minimum_required(VERSION 3.28) project(my_game LANGUAGES CXX) set(CHERYL_BUILD_TESTS OFF) set(CHERYL_BUILD_DEMO OFF) +set(CHERYL_BUILD_SDK OFF) set(CHERYL_BUILD_DEBUG_TERMINAL_LINUX OFF) add_subdirectory(path/to/cheryl-engine cheryl) add_executable(my_game main.cpp) @@ -39,7 +40,9 @@ Replace `path/to/cheryl-engine` with the checkout path relative to your applicat `CMakeLists.txt`, or an absolute path. CMake builds the selected libraries and propagates their public include paths and dependency settings. Reload the CMake project in the IDE after changing selections so completion uses those settings. -An SDK install is optional for this workflow. The [SDK build](building.md#development-sdk-builds) +Keep `CHERYL_BUILD_SDK=OFF` for source application builds; aggregate headers and IDE +completion do not require SDK export or installation. Enable it only when also +producing a separate SDK. The [SDK build](building.md#development-sdk-builds) collects headers and compiled libraries into a separate prefix for consumers that want a prebuilt distribution. @@ -131,6 +134,11 @@ configuration rather than mixing incompatible Debug/Release C++ binaries. 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. +For an HID-enabled SDK with an owned hidapi dependency, the selected backend, +headers and licenses are packaged. Linux consumers also need pkg-config and the +backend's libudev/libusb development libraries, plus Iconv when selected; the +package config discovers only the dependencies that backend requires. Packaging +does not resolve the [Linux controller limitations](../../projects/modules/platform/native-glfw/README.md#controller-backend-limits). 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. diff --git a/projects/modules/platform/native-glfw/CMakeLists.txt b/projects/modules/platform/native-glfw/CMakeLists.txt index 2bb80b66..09bf88b1 100644 --- a/projects/modules/platform/native-glfw/CMakeLists.txt +++ b/projects/modules/platform/native-glfw/CMakeLists.txt @@ -23,12 +23,15 @@ endif() cheryl_option(CHERYL_NATIVE_NULL_PLATFORM "Use GLFW's null platform without X11 or Wayland" ${cheryl_native_null_default}) if(NOT TARGET glfw) - set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) - if(CHERYL_NATIVE_NULL_PLATFORM) - set(GLFW_BUILD_X11 OFF) - set(GLFW_BUILD_WAYLAND OFF) - endif() - add_subdirectory("${CHERYL_REPOSITORY_ROOT}/extern/glfw" extern/glfw EXCLUDE_FROM_ALL) + block(SCOPE_FOR VARIABLES) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + set(GLFW_BUILD_DOCS OFF) + if(CHERYL_NATIVE_NULL_PLATFORM) + set(GLFW_BUILD_X11 OFF) + set(GLFW_BUILD_WAYLAND OFF) + endif() + add_subdirectory("${CHERYL_REPOSITORY_ROOT}/extern/glfw" extern/glfw EXCLUDE_FROM_ALL) + endblock() endif() file(GLOB_RECURSE cheryl_native_sources CONFIGURE_DEPENDS From 39422846b4a828bc24b30994801c6577ec7751e9 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 11:39:49 -0700 Subject: [PATCH 07/98] Updates agents.md --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index a0dad2d1..bf1fdca7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,8 @@ ### Work modes and progression +This section applies when in a model mode where multiple sub-agents cant be used frequently. When in such a mode this section provides valuable information worth considering, but documentation (to take an example) should not be deferred. + Each task has one primary work mode: - Review and planning From 450f528d7e97d02c921578b87579b7283f196530 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 12:12:09 -0700 Subject: [PATCH 08/98] Improves documentation text size and explains text rendering stages Scales documentation typography by twenty percent, including the RTD theme's fixed-size paragraphs and code blocks, and keeps navigation line heights readable through a dedicated stylesheet. Explains the first-game score label from a UTF-8 std::string through glyph selection and placement, CPU atlas images and triangles, graphics upload, and per-frame submission to the window. Defines shaping and atlas pages and clarifies the reason for separate stages, reuse and replacement. Validation: Sphinx configuration syntax, stylesheet registration and size overrides, tutorial local links/anchors/literalinclude paths, source-contract review and git diff --check. No documentation generation, project builds or tests were run. --- docs/generation/conf.py | 2 +- docs/generation/static/typography.css | 53 +++++++++++++++++++++++++++ docs/getting-started/first-game.md | 40 +++++++++++++------- 3 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 docs/generation/static/typography.css diff --git a/docs/generation/conf.py b/docs/generation/conf.py index 78f5921c..517eb184 100644 --- a/docs/generation/conf.py +++ b/docs/generation/conf.py @@ -95,7 +95,7 @@ html_theme = "sphinx_rtd_theme" templates_path = ["templates"] html_static_path = ["static"] -html_css_files = ["hierarchy.css"] +html_css_files = ["hierarchy.css", "typography.css"] html_title = "Cheryl Engine documentation" html_theme_options = {"navigation_depth": 4, "collapse_navigation": False} html_show_sourcelink = True diff --git a/docs/generation/static/typography.css b/docs/generation/static/typography.css new file mode 100644 index 00000000..1f6bf6a9 --- /dev/null +++ b/docs/generation/static/typography.css @@ -0,0 +1,53 @@ +/* Enlarge the RTD theme's type by 20%, including its fixed-size text. */ +html { + font-size: 120%; +} + +/* Preserve the theme's 16px body and 12px code proportions at the new scale. */ +p { + font-size: 1rem; + line-height: 1.5; +} + +.rst-content .linenodiv pre, +.rst-content div[class^="highlight"] pre, +.rst-content pre.literal-block { + font-size: 0.75rem; +} + +.wy-menu-vertical a { + line-height: 1.25; +} + +.wy-switch span { + font-size: 0.75rem; +} + +/* These theme icons also use fixed pixel sizes rather than inherited sizes. */ +.breadcrumb-item::before { + font-size: 0.8125rem; +} + +.rst-content .code-block-caption .headerlink, +.rst-content .eqno .headerlink, +.rst-content .toctree-wrapper > p.caption .headerlink, +.rst-content dl dt .headerlink, +.rst-content h1 .headerlink, +.rst-content h2 .headerlink, +.rst-content h3 .headerlink, +.rst-content h4 .headerlink, +.rst-content h5 .headerlink, +.rst-content h6 .headerlink, +.rst-content p.caption .headerlink, +.rst-content p .headerlink, +.rst-content table > caption .headerlink { + font-size: 0.875rem; +} + +.rst-content table > caption .headerlink::after { + font-size: 0.75rem; +} + +.wy-nav-top i { + font-size: 1.875rem; +} diff --git a/docs/getting-started/first-game.md b/docs/getting-started/first-game.md index 097b1b1f..43d7e5b6 100644 --- a/docs/getting-started/first-game.md +++ b/docs/getting-started/first-game.md @@ -224,9 +224,11 @@ that backend has been destroyed. See the ## 6. Select a font and prepare the score label -Font selection chooses immutable font bytes and faces. It does not draw text or -upload a glyph atlas. Select a preferred font name, lay out the message, -prepare its pixels and geometry, then upload that complete value: +A `std::string` containing `"Score: 0"` supplies UTF-8 bytes. To display it, Cheryl +turns those bytes into font shapes, then pixels and triangles that the renderer +can draw. Pass that string directly to `layout_text(fonts, message, options)`; +the string literal in this example works the same way. The example prepares and +uploads the label during `init()`: ```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp :language: cpp @@ -235,6 +237,20 @@ prepare its pixels and geometry, then upload that complete value: :dedent: 8 ``` +Here is the path from that string to the window: + +| Stage | What it produces and why | +| --- | --- | +| `FontCollection::load(selection)` | Font data describing how letters and symbols look, with fallback fonts for missing characters. | +| {cpp:func}`CE::Text::layout_text` | **Glyph choices and positions** at the requested font size. A glyph is a font's drawing for a letter, symbol, or group of characters. Choosing those drawings and adjusting their spacing is called **shaping**; for example, letters can join or share a glyph. Layout also handles text direction and line breaks. It produces no pixels. | +| {cpp:func}`CE::Text::Rendering::prepare` | CPU images of the needed glyphs, packed into image sheets called **atlas pages**, plus triangles that select each glyph's image. This turns the font's outlines into drawable pixels. | +| {cpp:func}`CE::Text::Rendering::upload` | A {cpp:class}`CE::Text::Rendering::Generation`: the layout paired with graphics resources created through the active resource provider. For this OpenGL application, the images and triangles become GPU textures and buffers. | +| `resolve_text()` and `pass.add()` in [step 8](#8-submit-the-sprite-and-text-as-a-complete-frame) | Per-frame drawing instructions combining the uploaded label with its material and position in the window. The renderer executes the submitted frame to display it. | + +These stages separate reusable text content from its placement in each frame. +Layout and preparation can run on a CPU worker. Upload must run on the thread +that owns the graphics context, as `init()` does here. + {cpp:class}`CE::Text::FontCollection` resolves the preferred `DejaVu Sans` family from the selected application asset root first, then the system's font directories, and appends the embedded fallback. The family name comes from the font's metadata; @@ -249,16 +265,14 @@ installed font share the requested name, the asset version wins. See the [font-selection example](../assets/text-layout.md#font-selection) for several ordered preferences and direct file selection. -{cpp:func}`CE::Text::layout_text` shapes the UTF-8 message using the selected -font data. {cpp:func}`CE::Text::Rendering::prepare` creates owned CPU atlas pages and -glyph geometry. {cpp:func}`CE::Text::Rendering::upload` uses the resource provider to -create a {cpp:class}`CE::Text::Rendering::Generation` generation. Initial upload is safe -here because `init()` runs on the graphics owner. - -The retained text owns the layout and matching resources it needs after the local -font and preparation variables leave scope. Keep this static label as one -generation. When gameplay starts changing the score, prepare a replacement and -adopt it only when upload succeeds. The +Keep `score_` and reuse it every frame. Moving the label only changes the draw +style in step 8; it does not require repeating layout, preparation, or upload. +When the message, selected fonts, or layout font size changes, repeat those three +stages and replace `score_` only after upload succeeds. Changing the original +string alone does not update an existing label: the layout retains its own +results rather than borrowing the string. The uploaded label retains the font +data and matching resources after local variables leave scope, while the engine's +backend must remain alive to draw them. The [Unicode text guide](../assets/text-layout.md) covers fallback, wrapping, and replacement ownership. From 3730085f670c103598cbcbb8183379cb9cac2c59 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 14:23:50 -0700 Subject: [PATCH 09/98] Restructures getting started around game lifecycle and complete use cases Adds project setup covering source and SDK consumption, artifact selection, public targets, and matching headers with binary configurations. Teaches startup, simulation ownership, frame publication, and resource lifetime before complete sprite and text paths. Labels maintained code excerpts by lifecycle hook and thread, preserves standalone example builds, and updates documentation navigation. --- README.md | 2 +- docs/README.md | 8 +- docs/development/consuming-engine.md | 7 +- docs/getting-started/first-game.md | 702 ++++++++++++++------------ docs/getting-started/project-setup.md | 264 ++++++++++ docs/index.md | 8 +- 6 files changed, 673 insertions(+), 318 deletions(-) create mode 100644 docs/getting-started/project-setup.md diff --git a/README.md b/README.md index 080325a0..72214f7d 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ for the steps required to add an owner. | **CMake configuration** and **builds** | [Options and defaults](docs/development/building.md#configuration-options), [build configurations](docs/development/building.md#build-configurations) and [engine-only builds](docs/development/building.md#engine-only-configuration) | | Dependencies | [SDK selection and toolchain settings](docs/development/building.md#dependency-and-toolchain-settings), [system requirements](docs/development/building.md#system-dependencies) and [public headers and link requirements](docs/development/consuming-engine.md#dependencies-and-headers) | | Targets and artifacts | [Module targets and aliases](projects/modules/README.md), [test targets](docs/development/testing.md#runner-selection) and [artifact name definitions](cmake/CherylOutputs.cmake) | -| Writing a game | [First-game walkthrough](docs/getting-started/first-game.md), [application composition and startup](docs/development/consuming-engine.md) and [game hooks and ownership](docs/development/consuming-engine.md#game-hooks-and-ownership) | +| Writing a game | [Project setup](docs/getting-started/project-setup.md), then [First-game walkthrough](docs/getting-started/first-game.md); [consumption reference](docs/development/consuming-engine.md) | | Documentation generation and hosting | [Local generation, shared tools and versioned Read the Docs setup](docs/development/documentation.md), [generated API entry points](docs/api/index.md) | | Adding a module | [Module authoring](docs/development/modules.md#add-a-module), [standalone composition](docs/development/modules.md#standalone-modules) and [UI adapters](docs/development/ui-adapters.md) | | Platform and graphics APIs | [Native GLFW](projects/modules/platform/native-glfw/README.md) and [OpenGL](projects/modules/graphics/opengl/README.md) | diff --git a/docs/README.md b/docs/README.md index 0bda30e3..a17a4800 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,7 @@ Choose an entry point for the work you are doing: | --- | --- | | Check implemented, partial, planned or deferred capabilities | [Status inventory and review baseline](STATUS.md) | | Understand the engine | [Runtime architecture](runtime/runtime-architecture.md) and [module roles](../projects/modules/README.md) | -| Write a game | [First-game walkthrough](getting-started/first-game.md), then [application composition and game hooks](development/consuming-engine.md) | +| Write a game | [Project setup](getting-started/project-setup.md), then [First-game walkthrough](getting-started/first-game.md) | | Browse the generated API or build the manual | [API entry points](api/index.md) and [documentation tooling and publishing](development/documentation.md) | | Configure, build or troubleshoot | [Setup](../README.md#setup) and [build reference](development/building.md) | | Run checks | [Test selection](development/testing.md) and [pending checks and QA](testing-requests.md) | @@ -16,6 +16,11 @@ Choose an entry point for the work you are doing: The system references below describe API contracts, ownership, units and failure behavior. Read the relevant subject rather than the whole documentation tree. +## Getting started + +- [Project setup, CMake, and source/SDK choices](getting-started/project-setup.md) +- [First game: startup, architecture, sprites, and text](getting-started/first-game.md) + ## Runtime - [Runtime architecture and backend boundaries](runtime/runtime-architecture.md) @@ -55,7 +60,6 @@ behavior. Read the relevant subject rather than the whole documentation tree. ## Development -- [First-game walkthrough](getting-started/first-game.md) - [Generated C++ API entry points](api/index.md) - [Documentation generation and versioned Read the Docs publishing](development/documentation.md) - [Pending automated and QA testing requests](testing-requests.md) diff --git a/docs/development/consuming-engine.md b/docs/development/consuming-engine.md index 9ca867ed..fbd21517 100644 --- a/docs/development/consuming-engine.md +++ b/docs/development/consuming-engine.md @@ -1,8 +1,9 @@ # Consuming the engine -Start with the [first-game walkthrough](../getting-started/first-game.md) for a -maintained application covering startup, controls, asset loading, and sprite/text -submission. This page describes the source and SDK consumption contracts. +Start with [project setup](../getting-started/project-setup.md) to choose source +or SDK consumption, then follow the [first-game walkthrough](../getting-started/first-game.md) +from startup and resource ownership through sprite/text submission. This page +provides the detailed source and SDK consumption contracts. Applications can compose source targets from a checkout or link a prebuilt [development SDK](#prebuilt-development-sdks). Link `Cheryl::Cheryl` to use the diff --git a/docs/getting-started/first-game.md b/docs/getting-started/first-game.md index 43d7e5b6..aea127cb 100644 --- a/docs/getting-started/first-game.md +++ b/docs/getting-started/first-game.md @@ -1,47 +1,26 @@ # Your first game -Start with a small application that opens a window, loads its own sprite and -shaders, and draws the sprite and a score label. The Left and Right arrow keys -move the sprite, Space selects its other image cell, and Escape closes the -application. The score stays at zero while you establish the application and -resource flow. - -The maintained source lives in -[projects/apps/first-game](../../projects/apps/first-game/). The excerpts below -come directly from that example, so you can inspect the complete application as -you work through each step. - -## 1. Set up the checkout and build the application - -Install Git, CMake 3.28 or newer, Ninja, and a C++23 compiler and standard library -with `std::format`. Follow the -[system dependency table](../development/building.md#system-dependencies) for -FreeType, HarfBuzz, ICU, and the native GLFW/OpenGL requirements. On Linux, this -example uses X11 and needs a desktop driver supporting an OpenGL 3.3 core context. -Python and Jinja2 are required when GLAD is generated during the source build. - -Choose a checkout location and a revision matching the documentation you are -reading. Use `main` for stable documentation, `develop` for latest documentation, -or the corresponding release tag for a versioned manual. Replace -`/path/to/cheryl-engine` with your desired checkout location: +Build a small game that opens a window, moves a sprite with the arrow keys, +changes its image with Space, and displays `Score: 0`. Escape closes the window. +The score stays at zero while you establish the application and resource flow. -```sh -( - set -e - cheryl_version=main - git clone --branch "$cheryl_version" https://github.com/cppcooper/cheryl-engine.git /path/to/cheryl-engine - cd /path/to/cheryl-engine - git submodule update --init --recursive -) -``` +Start with setup and `main()`, then learn which parts of the engine your game +uses and who owns its state. The sprite and text walkthroughs apply those rules +from initialization through drawing and cleanup. All excerpts come from the +maintained [first-game application](../../projects/apps/first-game/); they are +parts of the same implementation, with the enclosing function and thread named +above each excerpt. + +## Setup -The example's PNG, shader sources, and manifests are checked in under its own -`assets` directory. The Engine's embedded fallback font comes from the pinned -assets submodule. This walkthrough needs no optional artwork downloads or asset -initialization script. +Follow [Project setup](project-setup.md) for dependencies, a game CMake project, +source or SDK consumption, and the commands that produce libraries, an SDK, or +documentation. Building your game against a source checkout also builds the +libraries it needs; a separate SDK build is optional. -Run the following block from anywhere inside the checkout. It selects only the -native graphics/input composition and this application: +To run the maintained example in the checkout first, use this block from anywhere +inside that checkout. It selects the native graphics/input composition and the +first-game executable: ```sh ( @@ -63,88 +42,235 @@ native graphics/input composition and this application: ) ``` -`CMAKE_POLICY_VERSION_MINIMUM=3.5` accommodates the pinned legacy dependency -projects on CMake 4 hosts. `GLFW_BUILD_WAYLAND=OFF` selects the Linux X11 route; -`GAINPUT_ENABLE_HID=OFF` avoids the separate HID dependency download. See the -[build reference](../development/building.md) for other platform and toolchain -choices. Windows executables have an `.exe` suffix. +The policy setting accommodates pinned legacy dependencies on CMake 4 hosts. +The GLFW setting chooses X11 on Linux; the Gainput setting avoids the optional +HID dependency download. See the [build reference](../development/building.md) +for other platform choices. Windows executables have an `.exe` suffix. -The expected result is a 640 × 360 window containing a cyan sprite and -"Score: 0". Space changes the sprite to amber. Check movement, cell selection, -resizing, and Escape on the desktop; compilation and command-line help alone do +The example's PNG, shaders, and manifests live in its own `assets` directory; the +embedded fallback font comes from the pinned assets submodule. No optional +artwork downloads are needed. Expect a 640 × 360 window with a cyan sprite and +the score label. Check movement, Space changing the sprite to amber, resizing, +and Escape on the desktop; compiling or displaying command-line help alone does not establish those graphics and input observations. -## 2. Start the engine and keep its owners alive +## Main: configure and start the engine + +### Startup options, including your own -The application's entry point creates -{cpp:class}`CE::Application::Startup` through -{cpp:func}`CE::Application::GLFWOpenGL::make_startup`. That factory selects the native -GLFW window/input adapter and the OpenGL renderer and resource provider. +{cpp:func}`CE::Application::GLFWOpenGL::make_startup` selects the native GLFW +window/input adapter and OpenGL rendering/resource adapters. Its configuration +provides the application's defaults. Register custom command-line options before +calling `initialize()`: ```{literalinclude} ../../projects/apps/first-game/src/main.cpp :language: cpp +:caption: main() — platform thread; owners live through runtime.run() :start-after: // [startup] :end-before: // [/startup] :dedent: 8 ``` -Register your own options before calling `initialize()`. Here, `--assets` binds a -string that stays alive until parsing completes. Its default is an absolute path -supplied by the example's CMake target, so launching the executable from another -directory still selects the same assets. An explicit relative override resolves -from the application's working directory. - -Help and argument errors return a {cpp:struct}`CE::Application::StartupResult` with no -context; return its exit code when `should_start()` is false. On success, construct -your game from the owned context and use `make_runtime(game)` to apply the parsed -execution, input polling, and timing options together. - -Declare the result, game, and runtime in that order. The game borrows the -{cpp:class}`CE::Runtime::Context`, and the runtime borrows both. Their -declaration order keeps those owners alive through runtime destruction. Add -`--concurrent` when launching to place simulation on a separate thread; the same -game hooks and resource ownership rules apply. - -## 3. Give the game its hooks and controls - -Implement {cpp:struct}`CE::Game::AbstractGame` and keep the state your game -needs in that object. The example owns its camera, bindings, resources, and actor: +The factory's first argument is the command-line description. The configuration +sets a 640 × 360 window and its title. Cheryl also registers engine/backend +options: use `--help` to see those available in this build. For example, +`--window-width` overrides the initial width, `--worker-count` controls CPU worker +capacity, and `--concurrent` selects the separate simulation thread. + +`add_application_options()` lets your game use the same CLI11 parser. Here, +`--assets` writes into `asset_root`, which stays alive through parsing. CMake +supplies its default as an absolute path, so launching from another directory +still finds the same assets. An explicit relative override resolves from the +process's working directory. Add other game-specific options in that callback +and keep their destination variables alive until `initialize()` returns. + +### Configure the context and run the game + +The factory's {cpp:struct}`CE::Application::GLFWOpenGL::Options` configuration +selects the initial window dimensions, title, window mode, and swap interval. +Its `execution` member configures the context's CPU execution service, including +worker count and an optional shared pool. The factory also accepts +{cpp:struct}`CE::Application::RuntimeOptions` for loop defaults such as execution +mode and simulation timing. Parsed command-line options override these defaults. +See [application startup](../development/consuming-engine.md#command-line-startup) +for the complete option and validation contract. + +`initialize(argc, argv)` parses and validates options, then creates the owned +{cpp:class}`CE::Runtime::Context` and native window with the chosen adapters and +services. Help and argument errors return without a context: check +`should_start()` before using it and return the supplied exit code. Backend +creation failures can throw; the complete example wraps `main()`'s body in an +exception handler. + +Construct your game with that context, then use `result.make_runtime(game)` to +apply the parsed loop settings together. `runtime.run()` starts the rendering +and input adapters, calls the game hooks, runs until stopped, and shuts those +adapters down. Resource upload becomes available inside `Game::init()`. + +Declare the startup result, game, and runtime in that order. The game borrows the +context; the runtime borrows both. Reverse destruction order keeps each owner +alive for its borrowers. The context provides services; the runtime owns the +loop that calls your game. + +## Engine architecture: the game developer's view + +### Game state and lifecycle + +Implement {cpp:struct}`CE::Game::AbstractGame`. Keep the camera, actors, selected +resources, controls, and other game state in your implementation: ```{literalinclude} ../../projects/apps/first-game/src/first-game.h :language: cpp +:caption: Game — initial state is prepared in init(), then managed by simulation :start-after: // [game] :end-before: // [/game] :dedent: 4 ``` -| Hook | What belongs here | -| --- | --- | -| `init()` | Bind controls and create initial resources after the platform and graphics adapters start. | -| `update(tick)` | Advance game state using the supplied input and simulation delta. | -| `prepare_render_frame(frame) const` | Resolve that state into complete render submissions. | -| `quiesce()` | Stop external producers before final cleanup when your game adds them. | -| `deinit()` | Release game-owned resources and registrations after simulation and accepted work settle. | +The **platform thread** is the thread calling `runtime.run()`. It polls native +events and owns the graphics context. The **simulation owner** runs your game +updates and frame preparation. By default, it is that same thread. With +`--concurrent`, it is one dedicated simulation thread; your two simulation hooks +still run in order on that owner. -`init()` and `deinit()` run on the platform/graphics owner. In concurrent mode, -`update()` and frame preparation run on the simulation thread. Bindings belong to -the platform owner, so define them during initialization: +| Stage, in lifecycle order | Owner | Your responsibility | +| --- | --- | --- | +| `init()` | Platform/graphics thread, before simulation starts | Bind controls, acquire resources, and prepare initial game state. | +| `update(tick)` | Simulation owner | Change game state using copied input, dimensions, and simulation time. Request a stop when appropriate. | +| `prepare_render_frame(frame) const` | Same simulation owner | Turn current state into a complete frame of draw values and retained resources. Keep gameplay progression in `update()`; frame preparation can be skipped when no frame slot is available. | +| `quiesce()` | Platform thread, after simulation stops and joins | Detach external listeners and stop producers that could submit more work. The default hook is enough for this example. | +| `deinit()` | Platform/graphics thread, after accepted work settles and frames are recycled | Remove bindings and release game-owned resources while input and graphics still exist. | + +Initialization prepares the state on the platform thread before simulation +begins. During the loop, simulation owns changes to that state. Rendering reads a +**published frame**: copied camera/draw values and retained resource handles. +After your frame-preparation hook returns, the runtime publishes the completed +frame. + +That boundary lets native events and graphics stay on their required thread +while simulation can run independently. Reading a live actor position from the +renderer while simulation changes it would be a data race. Instead, the renderer +can draw an earlier frame's copied position while the actor moves again. +`shared_ptr` keeps a resource alive; it does not make concurrent mutation safe. +Publish a new resource version when its contents change. + +### Interfaces you use + +| Interface | When and why your game uses it | +| --- | --- | +| `Context::input()` | Bind actions in `init()` and unbind them in `deinit()`. Read gameplay input from `tick.input` during updates. | +| `Context::window()` | Read native window state on the platform thread. During updates, use `tick.framebuffer_size`, the copied dimensions for simulation. | +| `Context::resources()` | Upload prepared graphics resources on the graphics owner, initially in `init()`. | +| `TickContext` | Read the simulation delta/input/dimensions and call `request_stop()`. Its borrowed data is valid for that update. | +| `FrameWriter` and its passes | Build a CPU-side frame during `prepare_render_frame()`. Do not retain the writer or passes after the hook returns. | +| `Context::make_worker_group()` and `platform_dispatcher()` | Later, prepare expensive resources on tracked CPU workers and hand owned upload requests to the platform thread. | + +Calling a context accessor does not move execution to another thread. In +particular, graphics uploads in concurrent `update()` would run on the wrong +thread. Your game supplies frame data; the runtime calls the renderer and +presents the window. The +[runtime architecture](../runtime/runtime-architecture.md) and +[frame boundary](../runtime/runtime-frame-boundary.md) describe the wider design. + +### Obtain, use, and stop using resources + +Separate shared resource contents from the mutable state that uses them. A +sprite contains image/grid data; each actor owns its position and selected cell. +A text generation contains an uploaded label; the game chooses its position and +which generation to submit. Keep those choices with simulation. + +| Resource | Obtain | Use | Stop using | +| --- | --- | --- | --- | +| Sprite and material | Load graphics assets through the provider, then retain named cache results. | Simulation chooses a cell/transform; frame preparation submits those values and retains the resources. | Stop submitting them and reset the game's handles during cleanup or replacement. | +| Font and rendered text | Load a font collection, lay out the message, prepare CPU images/triangles, then upload a text generation. | Retain the generation and resolve it into each frame that displays the label. | Replace or reset the generation; local font/layout variables need not remain in scope. | +| Sound effect or music | With the optional audio module, decode a clip, then play it through an application-owned audio system; or open a stream there. | Playback voices run on the audio system's own timeline; they do not pass through rendering frames. | Explicitly stop a voice when playback should end; dropping a voice handle alone does not stop it. Close the audio system after its producers stop. | + +Releasing a game handle ends that owner's use. Caches and published frames may +still retain the resource, so it does not promise immediate native destruction. +Do not clear a shared cache just because an actor disappears. Keep the originating +backend alive until its resources have been released and retired; runtime +shutdown provides that ordering for this game's graphics. See +[resource ownership](../resources/resource-residency.md) and +[audio playback](../runtime/audio.md) for their respective lifetime contracts. + +## Prepare controls and a drawing pass + +These are shared foundations for the sprite and text examples below. + +### Bind controls during initialization + +Native key codes become semantic game actions at the input boundary. Retain the +returned IDs so cleanup can remove this game's bindings: ```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp :language: cpp +:caption: Inside Game::init() — platform thread :start-after: // [bindings] :end-before: // [/bindings] :dedent: 8 ``` -The native key codes appear at this boundary. Gameplay reads the game's semantic -{cpp:struct}`CE::Input::Actions::Id` values instead. Retaining each returned binding -ID lets the game remove its own mappings during cleanup. The -[input guide](../runtime/input-state-model.md) covers additional actions, events, -and focus handling. +Updates read the action IDs (`left_`, `right_`, `next_cell_`, `quit_`) rather than +native key codes. See the [input guide](../runtime/input-state-model.md) for other +actions, events, and focus handling. + +### Understand what a draw needs + +A sprite or uploaded label supplies geometry and images. A **material** supplies +the drawing recipe: shader program, geometry topology, sampling, and parameter +bindings. The example loads both recipes from its +[material manifest](../../projects/apps/first-game/assets/graphics/shaders/materials.json): + +| Material | Geometry and sampling | +| --- | --- | +| `first-game:images` | Four-vertex triangle strips for sprite cells; nearest sampling for the small image. | +| `first-game:text` | Six-vertex glyph quads as triangles; linear sampling for the text's alpha atlas. | + +The camera converts Y-up pixel coordinates into the renderer's view. Initialize +it from the actual framebuffer dimensions before placing the actor: + +```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp +:language: cpp +:caption: Inside Game::init() — platform/graphics thread, before bindings and asset loading +:start-after: void Game::init() { +:end-before: // [bindings] +:dedent: 8 +``` + +Updates will keep the camera's dimensions current using the tick's copied size. +During frame preparation, open a pass with that camera and describe its common +image parameter: + +```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp +:language: cpp +:caption: Inside Game::prepare_render_frame() — simulation owner; common to both draws +:start-at: auto pass = frame.begin_pass +:end-before: CE::Rendering::DrawStyle2D image_style; +:dedent: 8 +``` + +`begin_pass()` copies the {cpp:class}`CE::Rendering::Camera2D` projection and view +matrices. +{cpp:struct}`CE::Rendering::Submission::Context2D` describes the pass values and +selects the public `image` parameter at texture unit zero. Each draw then adds a +{cpp:struct}`CE::Rendering::DrawStyle2D` with its material, model transform, scale, +and alpha. The following two walkthroughs add to this same pass. + +The material maps public parameter names to native shader uniforms: the code +uses `image`, while this OpenGL recipe calls that sampler `mytexture`. The +[vertex shader](../../projects/apps/first-game/assets/graphics/shaders/first-game.vert) +combines the pass's projection/view with each draw's model transform. See +[pipelines and materials](../rendering/pipelines-and-materials.md) when writing +your own recipe. + +## Put a sprite on screen -## 4. Load a controlled game asset directory +The complete path is: **load image/grid/material → retain a sprite in an actor → +update position/cell → submit a draw → release the actor and material**. -Put the resources required by this game under one root. The example uses: +### Init: load assets and create the actor + +Keep this game's resources under one controlled asset root: ```text assets/ @@ -157,284 +283,242 @@ assets/ └── textures/actor.png ``` -The index selects the sprite and shader/material definitions: +The index chooses which definition files to load: ```{literalinclude} ../../projects/apps/first-game/assets/graphics/graphics-manifests.json :language: json +:caption: graphics-manifests.json — index for this asset root ``` -Only files named `graphics-manifests.json` select definitions automatically. -Unlisted JSON files are ignored. The listed paths, texture references, and shader -source references resolve from the enclosing `graphics` directory. The `$schema` -URLs support editor validation; runtime loading uses local files. +Only files named `graphics-manifests.json` select definitions automatically; +unlisted JSON files are ignored. Paths in those definitions resolve from their +enclosing `graphics` directory. `$schema` URLs help the editor validate JSON; +runtime loading uses the local files. -Create an owned {cpp:struct}`CE::Assets::Loading::Loader` and load the root during `init()`: +The sprite definition divides a 32 × 16 image into two 16 × 16 cells: -```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp -:language: cpp -:start-after: // [assets] -:end-before: // [/assets] -:dedent: 8 +```{literalinclude} ../../projects/apps/first-game/assets/graphics/definitions/sprites.json +:language: json +:caption: Sprite definition — two selectable cells with a centered pivot ``` -`load_assets()` performs CPU preparation followed by upload on the active -provider's loading owner. It processes indexed programs/materials, images, -sprites, and tilesets, along with standalone PNGs below the root. Fonts and audio -use separate loading paths. Preparation validates definitions and image bounds -before publication; an upload failure can leave already completed cache entries. - -Use a controlled root containing just this game's required assets. Pointing the -general loader at the repository's larger asset collection also selects its -optional downloaded artwork. `register_files()` only registers paths for manual -loading; it does not parse definitions, decode images, or upload resources. -The [asset-loading guide](../assets/asset-loading.md) explains publication and -retry behavior. - -Resolve required named resources during initialization and check each result. -{cpp:struct}`CE::Assets::Sprites::Manager` returns a retained sprite handle or null; -{cpp:struct}`CE::Assets::Shaders::Manager` supplies the materials used for drawing. -For this manifest, `get_asset("first-game", "actor")` identifies -`first-game:actor` exactly. `get_assets("first-game")` provides a namespace -collection keyed by local names when your game needs several sprites. - -## 5. Pass retained resources into game-owned state - -The sprite is shared resource data. The actor owns its position and selected -cell, while retaining the sprite passed into its constructor: +An actor retains that shared sprite while keeping its position and current cell +as game-owned values: ```{literalinclude} ../../projects/apps/first-game/src/first-game.h :language: cpp +:caption: Actor — resource handle plus per-actor simulation state :start-after: // [actor] :end-before: // [/actor] :dedent: 4 ``` -The `actor_.emplace(...)` call above transfers a retained handle into this small -game-owned structure. Use the same approach when an enemy manager or another -game system needs a sprite collection: resolve the required names once, then -pass the handles into that system. Keep per-entity transforms, counters, and -playback state with their application owners. A namespace collection's iteration -order supplies no gameplay identity. - -The loader can go out of scope after initialization. Cache lookups and the -actor's retained handles have their own lifetimes. The resources still belong to -their original backend domain; retaining a handle does not make it usable after -that backend has been destroyed. See the -[resource ownership guide](../resources/resource-residency.md). - -## 6. Select a font and prepare the score label - -A `std::string` containing `"Score: 0"` supplies UTF-8 bytes. To display it, Cheryl -turns those bytes into font shapes, then pixels and triangles that the renderer -can draw. Pass that string directly to `layout_text(fonts, message, options)`; -the string literal in this example works the same way. The example prepares and -uploads the label during `init()`: +Load the root, check the required named resources, and construct the actor: ```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp :language: cpp -:start-after: // [score] -:end-before: // [/score] +:caption: Inside Game::init() — platform/graphics thread +:start-after: // [assets] +:end-before: // [/assets] :dedent: 8 ``` -Here is the path from that string to the window: +`load_assets()` prepares CPU data and uploads through the active resource +provider. It loads indexed graphics definitions and standalone PNGs under the +root; fonts and audio have separate paths. Keep the root limited to this game's +assets, so loading does not also select unrelated or optional artwork. +`register_files()` alone only registers paths; it does not perform this loading. +An upload failure can leave already completed cache entries; the +[asset-loading guide](../assets/asset-loading.md) explains validation and retry. -| Stage | What it produces and why | -| --- | --- | -| `FontCollection::load(selection)` | Font data describing how letters and symbols look, with fallback fonts for missing characters. | -| {cpp:func}`CE::Text::layout_text` | **Glyph choices and positions** at the requested font size. A glyph is a font's drawing for a letter, symbol, or group of characters. Choosing those drawings and adjusting their spacing is called **shaping**; for example, letters can join or share a glyph. Layout also handles text direction and line breaks. It produces no pixels. | -| {cpp:func}`CE::Text::Rendering::prepare` | CPU images of the needed glyphs, packed into image sheets called **atlas pages**, plus triangles that select each glyph's image. This turns the font's outlines into drawable pixels. | -| {cpp:func}`CE::Text::Rendering::upload` | A {cpp:class}`CE::Text::Rendering::Generation`: the layout paired with graphics resources created through the active resource provider. For this OpenGL application, the images and triangles become GPU textures and buffers. | -| `resolve_text()` and `pass.add()` in [step 8](#8-submit-the-sprite-and-text-as-a-complete-frame) | Per-frame drawing instructions combining the uploaded label with its material and position in the window. The renderer executes the submitted frame to display it. | - -These stages separate reusable text content from its placement in each frame. -Layout and preparation can run on a CPU worker. Upload must run on the thread -that owns the graphics context, as `init()` does here. - -{cpp:class}`CE::Text::FontCollection` resolves the preferred `DejaVu Sans` family -from the selected application asset root first, then the system's font directories, -and appends the embedded fallback. The family name comes from the font's metadata; -it need not match its filename. A filename such as `DejaVuSans.ttf` can also select -an application font nested anywhere below `selection.asset_directories`. - -The engine discovers installed fonts automatically. `automatic_system_fonts=false` -disables additional automatic family choices; explicitly preferred names still -resolve against both application and system fonts. Missing names are skipped, so -the score remains usable without that font installation. If an asset font and an -installed font share the requested name, the asset version wins. See the -[font-selection example](../assets/text-layout.md#font-selection) for several -ordered preferences and direct file selection. - -Keep `score_` and reuse it every frame. Moving the label only changes the draw -style in step 8; it does not require repeating layout, preparation, or upload. -When the message, selected fonts, or layout font size changes, repeat those three -stages and replace `score_` only after upload succeeds. Changing the original -string alone does not update an existing label: the layout retains its own -results rather than borrowing the string. The uploaded label retains the font -data and matching resources after local variables leave scope, while the engine's -backend must remain alive to draw them. The -[Unicode text guide](../assets/text-layout.md) covers fallback, wrapping, and -replacement ownership. - -## 7. Update simulation and select image cells - -Use {cpp:struct}`CE::Game::TickContext` during each update. It supplies the -simulation delta, accumulated semantic input, and copied window dimensions: +`get_asset("first-game", "actor")` looks up `first-game:actor` and returns a +retained handle or null. `get_assets("first-game")` returns that namespace keyed +by local name when a game system needs several sprites. Resolve names once and +pass handles into the owning actors/managers; collection iteration order is not +gameplay identity. The local loader can go out of scope after initialization. +The actor's retained handle remains valid within its backend's lifetime. + +### Update: move the actor and choose a cell ```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp :language: cpp +:caption: Game::update() — simulation owner, once per simulation step :start-after: // [update] :end-before: // [/update] :dedent: 4 ``` -Movement uses `delta_seconds`, and the camera follows the tick's framebuffer -size. This supports concurrent mode without querying a live window from the -simulation thread. Escape requests a stop through the tick; the runtime performs -its normal cleanup after the update returns. - -The sprite definition divides the 32 × 16 PNG into two 16 × 16 cells: +Movement uses `tick.delta_seconds`. The camera follows `tick.framebuffer_size` +without reading the live native window. Escape requests normal runtime shutdown. +Space advances the actor's selected image cell; the shared sprite stays unchanged. -```{literalinclude} ../../projects/apps/first-game/assets/graphics/definitions/sprites.json -:language: json -``` +This asset has no timed animation clip. For authored clips, keep a separate +{cpp:class}`CE::Assets::Sprites::Animation` cursor per actor and advance it with +simulation time. See [asset values and playback](../assets/asset-values-and-playback.md). -A grid describes which image rectangle each cell selects. Space advances the -actor's cell index explicitly. This manifest supplies no timed animation clip. -For a sprite with authored timed clips, keep a separate -{cpp:class}`CE::Assets::Sprites::Animation` cursor per entity and advance it using -simulation time. The shared cached sprite holds definitions and resources; -its entities choose playback. See -[asset values and playback](../assets/asset-values-and-playback.md). +### Prepare a frame: publish the actor's current appearance -## 8. Submit the sprite and text as a complete frame - -Frame preparation copies the simulation's current transforms and selects -retained resources. The renderer consumes those resolved packets later: +Use the common pass above to submit the current cell and transform: ```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp :language: cpp -:start-after: // [render] -:end-before: // [/render] -:dedent: 4 +:caption: Inside Game::prepare_render_frame() — simulation owner +:start-at: CE::Rendering::DrawStyle2D image_style; +:end-before: CE::Rendering::DrawStyle2D text_style; +:dedent: 8 ``` -{cpp:class}`CE::Rendering::Camera2D` uses Y-up pixel coordinates. Opening a pass copies its -projection and view matrices. The -{cpp:struct}`CE::Rendering::Submission::Context2D` carries those pass values and -selects the public `image` parameter at texture unit zero. Each -{cpp:struct}`CE::Rendering::DrawStyle2D` chooses a material and supplies the -draw's model transform, scale, and alpha. +{cpp:func}`CE::Rendering::Submission::resolve_sprite` builds CPU draw packets. +`pass.add()` copies the draw values and retains the geometry, material, and +selected image in the frame. After publication, the platform renderer draws +those packets and presents the window. The actor can move again without changing +the submitted frame. -The checked-in -[material manifest](../../projects/apps/first-game/assets/graphics/shaders/materials.json) -gives the shared shader program two compatible draw recipes: +### Deinit: release this game's use -| Material | Geometry topology | Why it is separate | -| --- | --- | --- | -| `first-game:images` | Triangle strip | Each sprite cell uses a four-vertex strip, with nearest sampling for the small image. | -| `first-game:text` | Triangles | Each uploaded glyph uses a six-vertex quad, with linear sampling for its alpha atlas. | +On the platform thread, `actor_.reset()` releases the actor and its sprite handle; +`images_.reset()` releases the game's image material handle. They appear in the +[complete cleanup hook](#finish-the-game-and-release-resources) below, together +with input and text cleanup. Earlier frames and cache ownership follow the +resource lifetime rules already described. -The vertex shader applies the copied pass and draw transforms: +## Put text on screen -```{literalinclude} ../../projects/apps/first-game/assets/graphics/shaders/first-game.vert -:language: glsl -``` +Start with a UTF-8 `std::string`, such as `std::string message = "Score: 0"`. +The complete path is: **choose fonts → choose/position glyphs → build their +images and triangles → upload → submit a draw → release the uploaded label**. +The string literal in this example follows that same path. -Material bindings map public parameter keys to the shader's native uniform names. -Your common submission code uses `image`, while this OpenGL recipe maps it to -`mytexture`. {cpp:func}`CE::Rendering::Submission::resolve_sprite` and -{cpp:func}`CE::Rendering::Submission::resolve_text CE::Rendering::Submission::resolve_text(const CE::Text::Rendering::Generation&, const DrawStyle2D&, const Context2D&)>` -resolve complete CPU packets without binding -graphics state or uploading resources. Adding those packets to the pass retains -their geometry, materials, and selected images and copies their parameter values. -The renderer receives no borrowed actor, camera, or playback cursor. The -[pipeline and material guide](../rendering/pipelines-and-materials.md) explains -the contracts and additional recipes. +### Init: turn the message into a drawable label -## 9. Release the game's resources +Each stage answers a different question: -The runtime joins simulation and recycles accepted frames before calling -`deinit()` on the platform/graphics owner. Remove the game's mappings and release -its retained generations there: +| Stage | What it produces and why | +| --- | --- | +| `FontCollection::load(selection)` | Font data describing letter and symbol shapes, with fallback fonts for missing characters. | +| {cpp:func}`CE::Text::layout_text` | **Which glyphs to draw and where to put them**, at the requested font size. A glyph is a font's drawing for a letter, symbol, or group of characters. Choosing glyphs and adjusting their spacing is called **shaping**; letters can join or share a glyph. Layout also handles direction and line breaks. It produces no pixels. | +| {cpp:func}`CE::Text::Rendering::prepare` | CPU images of the needed glyphs packed into sheets called **atlas pages**, plus triangles selecting each glyph's image. This turns font outlines into drawable pixels. | +| {cpp:func}`CE::Text::Rendering::upload` | A {cpp:class}`CE::Text::Rendering::Generation` holding the layout and graphics resources. In this application, images and triangles become OpenGL textures and buffers. | +| `resolve_text()` and `pass.add()` below | A frame's drawing instructions combining that uploaded label with a material and position. The runtime's renderer executes them to display the text. | + +The initialization code follows those stages. `resources` is the provider +reference obtained in the asset-loading excerpt: ```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp :language: cpp -:start-after: // [cleanup] -:end-before: // [/cleanup] -:dedent: 4 +:caption: Inside Game::init() — platform/graphics thread +:start-after: // [score] +:end-before: // [/score] +:dedent: 8 ``` -Cleanup also follows attempted initialization, so these resets tolerate resources -that were never created. The example uses the default `quiesce()` because it has -no external producers. Games that add listeners, audio producers, or background -services should stop those producers in `quiesce()` while their dependencies -remain alive. The [runtime frame boundary](../runtime/runtime-frame-boundary.md) -defines the complete shutdown ordering. - -## Continue with background loading - -The synchronous path makes initial ownership clear. For larger loads, use an -Engine worker group for `Loader::prepare()` and submit the resulting owned -`CE::Assets::Loading::Prepared` to the platform dispatcher for `upload()`. Simulation checks -futures for readiness and adopts complete retained results during update. Keep -the loader alive through preparation and handoff, and keep the prior resource -generation while its replacement is pending. Avoid waiting on the thread that -must complete the upload. - -Text follows the same boundary: font/layout preparation can run on workers; -upload remains on the graphics owner. Follow the working sequences in -[asset loading](../assets/asset-loading.md), -[worker execution](../runtime/worker-execution.md), and -[thread dispatch](../runtime/thread-dispatch.md) when you add that flow. This -example keeps initialization synchronous and frame preparation CPU-only. +These stages let you reuse prepared text while changing its placement. Layout +and preparation are CPU work; upload must run on the graphics owner, as it does +here during `init()`. Keep the resulting `score_` in the game for simulation to +select and publish in later frames. Local font/layout variables can leave scope; +the generation retains the data and resources it needs. -## Use the example as an independent project +The preferred `DejaVu Sans` name means a font **family from its metadata**, not a +system file path. Cheryl searches the selected asset directories recursively, +then installed system fonts, and appends its embedded fallback. Asset filenames +such as `DejaVuSans.ttf` also work as aliases. You do not supply a system font +directory. Asset fonts win name conflicts; missing preferences are skipped and +available fonts still load. -The example's own CMake entry point supports source and installed SDK consumers. -Its application target links the public owners explicitly: +`automatic_system_fonts=false` disables additional automatic family choices; +explicit preferences still resolve from assets and the system. See +[font selection](../assets/text-layout.md#font-selection) for ordered preferences, +direct files, and fallback coverage. -```{literalinclude} ../../projects/apps/first-game/CMakeLists.txt -:language: cmake -:start-at: add_executable -:end-before: Source Debug -``` +### Update: retain or replace the label -To configure the example directly against the checkout, use a separate build -directory. Its bootstrap selects the required Engine, Native GLFW, OpenGL, and -startup targets and disables unrelated applications and modules: +This example's message is static, so `update()` keeps the same `score_`. Moving +the label changes only its draw transform. When the message, fonts, or layout +font size changes, repeat layout, preparation, and upload, then let simulation +adopt the completed replacement generation. Changing the original string alone +does not update an uploaded label. -```sh -( - set -e - cd "$(git rev-parse --show-toplevel)" - cmake -S projects/apps/first-game -B build/first-game-source -G Ninja \ - -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ - -DCHERYL_FIRST_GAME_ENGINE_SOURCE="$PWD" \ - -DGLFW_BUILD_WAYLAND=OFF -DGAINPUT_ENABLE_HID=OFF - cmake --build build/first-game-source --target first-game --parallel - ./build/first-game-source/first-game -) +For a small initial label, synchronous `init()` is enough. For text that changes +during play, use the [background-loading handoff](#load-or-replace-resources-during-play) +below: prepare CPU data, dispatch upload to the graphics owner, then adopt its +result during `update()`. Keep the previous generation while the replacement is +pending; published frames retain the version they already selected. + +### Prepare a frame: place and publish the label + +Add the uploaded label to the same camera/pass used for the sprite: + +```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp +:language: cpp +:caption: Inside Game::prepare_render_frame() — simulation owner +:start-at: CE::Rendering::DrawStyle2D text_style; +:end-at: pass.add(CE::Rendering::Submission::resolve_text +:dedent: 8 ``` -For an installed SDK, leave `CHERYL_FIRST_GAME_ENGINE_SOURCE` empty and supply a -compatible SDK through `CMAKE_PREFIX_PATH`. It must include Native GLFW with -native input, OpenGL, and OpenGL startup. The SDK contains libraries and headers; -keep this example's source and `assets` directory alongside its CMake project: +The model transform places the label's origin 24 pixels from the left and 48 below +the framebuffer's top in the Y-up camera. The `first-game:text` material draws its +glyph triangles with the uploaded atlas images. +{cpp:func}`CE::Rendering::Submission::resolve_text CE::Rendering::Submission::resolve_text(const CE::Text::Rendering::Generation&, const DrawStyle2D&, const Context2D&)>` +performs CPU packet construction; it does not upload or draw. `pass.add()` +retains the selected resources and copies parameters into the frame. After the +hook returns, the runtime publishes the complete frame for the platform renderer +to draw and present. -```sh -( - set -e - cd "$(git rev-parse --show-toplevel)" - cmake -S projects/apps/first-game -B build/first-game-sdk -G Ninja \ - -DCMAKE_BUILD_TYPE=Release -DCHERYL_FIRST_GAME_ENGINE_SOURCE= \ - -DCMAKE_PREFIX_PATH=/absolute/path/to/extracted-sdk - cmake --build build/first-game-sdk --target first-game --parallel - ./build/first-game-sdk/first-game -) +### Deinit: release the uploaded label + +`score_.reset()` and `text_.reset()` release this game's generation and text +material on the platform thread during the cleanup hook below. Backend resource +retirement follows retained ownership. See the +[Unicode text guide](../assets/text-layout.md) for wrapping and more advanced +replacement behavior. + +## Finish the game and release resources + +The full cleanup hook removes the bindings and releases both examples' handles: + +```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp +:language: cpp +:caption: Game::deinit() — platform/graphics thread, after simulation and accepted work finish +:start-after: // [cleanup] +:end-before: // [/cleanup] +:dedent: 4 ``` -The application's compiled default asset path names its source `assets` -directory. Use `--assets` when relocating those files with a packaged application. -For SDK ABI/configuration matching and Windows runtime DLL paths, follow -[consuming the engine](../development/consuming-engine.md#prebuilt-development-sdks). +Cleanup also follows attempted initialization, so tolerate resources that were +never created. The runtime stops and joins simulation, calls `quiesce()`, settles +accepted work while servicing required platform dispatch, recycles frames, and +calls `deinit()` before input and graphics shutdown. + +This game uses the default `quiesce()` because it has no external producers. +When adding listeners or background services, detach/stop those producers there +while their dependencies still exist. Do not wait in `quiesce()` for work that +needs platform dispatch; settling accepted work follows that hook. The +[frame-boundary guide](../runtime/runtime-frame-boundary.md) defines the complete +shutdown and failure ordering. + +## Load or replace resources during play + +The same resource path can span threads when preparation is expensive: + +1. **Simulation:** request a load through an Engine worker group and keep the + current resource while the request is pending. +2. **CPU worker:** prepare owned data, such as `Loader::prepare()` results or a + text layout and atlas. Keep the loader and request inputs alive through their + work. +3. **Platform/graphics thread:** receive the owned prepared value through the + platform dispatcher, upload it, and return retained resources through a future. +4. **Simulation, during `update()`:** check readiness and adopt the completed + result. Later frames select the new version; earlier frames retain the old one. + +Pass owned request data across that boundary. A platform upload callback should +return a result rather than mutate live simulation state through a captured +`this`. Avoid blocking the thread needed to complete the upload. Tracked work and +the runtime shutdown sequence keep the graphics owner available while accepted +requests settle. + +Follow the working sequences in [asset loading](../assets/asset-loading.md), +[worker execution](../runtime/worker-execution.md), and +[thread dispatch](../runtime/thread-dispatch.md) when adding this flow. Continue +with [audio playback](../runtime/audio.md) for clips, streams, and voices, which +use the audio system's own lifetime and playback path. diff --git a/docs/getting-started/project-setup.md b/docs/getting-started/project-setup.md new file mode 100644 index 00000000..8c9229c6 --- /dev/null +++ b/docs/getting-started/project-setup.md @@ -0,0 +1,264 @@ +# Project setup + +Choose how your game obtains Cheryl before writing its entry point. During +development, adding a source checkout to your game's CMake project is sufficient: +building the game also builds the libraries it uses. An installed development SDK +is an alternative when you want to consume already compiled libraries. Both paths +provide the same CMake target names and public `cheryl/` headers. + +After setup, continue with [Your first game](first-game.md). It introduces startup, +the game hooks and resource ownership before putting a sprite and text on screen. +The [consumption reference](../development/consuming-engine.md) covers other target +compositions and the full SDK contract. + +## Dependencies + +Use CMake 3.28 or newer, Ninja, and a C++23 compiler and standard library with +`std::format` support. Install the +[system dependencies](../development/building.md#system-dependencies) required by +your selected modules. The first game uses FreeType, HarfBuzz and ICU for text, +plus Native GLFW, native input and OpenGL for its window, controls and rendering. +It needs a desktop graphics driver supporting an OpenGL 3.3 core context. + +For source builds, initialize the checkout's recorded recursive submodules. Python +and Jinja2 are needed when GLAD generates OpenGL bindings. The first game supplies +its own sprite, shaders and manifests; the embedded fallback font comes from the +assets submodule. No optional artwork download is needed. + +For an SDK, install its +[consumer prerequisites](../development/consuming-engine.md#prebuilt-development-sdks) +instead of rebuilding Cheryl. Some native development/runtime libraries remain +external on Linux; Windows SDKs include a dependency prefix whose DLLs must be +available at runtime. + +## Build your game with a source checkout + +A simple layout keeps your game independent of the engine checkout: + +```text +projects/ +├── cheryl-engine/ +└── my-game/ + ├── CMakeLists.txt + ├── src/ + │ ├── main.cpp + │ ├── first-game.h + │ └── first-game.cpp + └── assets/ +``` + +Clone Cheryl at a revision matching the manual you are reading: `main` for stable, +`develop` for latest, or the matching release tag. Replace `/path/to/projects` with +your chosen parent directory. This block preserves your shell's starting directory: + +```sh +( + set -e + cd /path/to/projects + git clone --branch main https://github.com/cppcooper/cheryl-engine.git + cd cheryl-engine + git submodule update --init --recursive +) +``` + +Copy the maintained [first game's](../../projects/apps/first-game/) `src/` and +`assets/` directories into `my-game/` to use its code unchanged. Create +`my-game/CMakeLists.txt` as follows. The options select the first game's +native graphics/input composition and omit unrelated applications and integrations. +The block scopes those settings to Cheryl; it does not force changes into your +project's CMake cache. + +```cmake +cmake_minimum_required(VERSION 3.28) +project(my_game LANGUAGES CXX) + +set(CHERYL_ENGINE_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/../cheryl-engine" + CACHE PATH "Cheryl Engine source checkout") + +block(SCOPE_FOR VARIABLES) + set(CHERYL_BUILD_NATIVE_GLFW ON) + set(CHERYL_BUILD_OPENGL ON) + set(CHERYL_NATIVE_INPUT ON) + set(CHERYL_NATIVE_NULL_PLATFORM OFF) + set(CHERYL_BUILD_TESTS OFF) + set(CHERYL_BUILD_CONSUMER_TESTS OFF) + set(CHERYL_BUILD_DEMO OFF) + set(CHERYL_BUILD_FIRST_GAME OFF) + set(CHERYL_BUILD_DOCS OFF) + set(CHERYL_BUILD_SDK OFF) + set(CHERYL_BUILD_DEBUG_TERMINAL_LINUX OFF) + set(CHERYL_BUILD_UI_TGUI OFF) + set(CHERYL_BUILD_UI_RMLUI OFF) + set(CHERYL_BUILD_AUDIO_MINIAUDIO OFF) + set(CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC OFF) + set(CHERYL_BUILD_WORKER_TOPOLOGY_JSON OFF) + add_subdirectory("${CHERYL_ENGINE_SOURCE}" cheryl EXCLUDE_FROM_ALL) +endblock() + +add_executable(my_game src/main.cpp src/first-game.cpp) +target_link_libraries(my_game PRIVATE Cheryl::Cheryl) +target_compile_definitions(my_game PRIVATE + CHERYL_FIRST_GAME_ASSETS="${CMAKE_CURRENT_SOURCE_DIR}/assets") +``` + +The scoped `set()` values fix this template's component selection; edit them +when adding a module. A same-name `-D` cache option does not override those local +values. + +`CHERYL_FIRST_GAME_ASSETS` supplies the example's default asset-root string, so +launching from an IDE or another directory still finds your game's files. As you +replace the example with your own game, keep that explicit connection between +the CMake target and the asset path used by startup. + +`Cheryl::Cheryl` provides Engine and the selected modules/support, including their +include directories, feature definitions and dependencies. Your sources can use +`#include ` without collecting individual include paths. Changing +a `CHERYL_BUILD_*` option makes a component available; linking its target is what +provides that component to the application. The facade links the configured set. +Reload CMake in the IDE after changing selections. + +Configure and build from the game directory. Replace +`/path/to/projects/my-game` with its location: + +```sh +( + set -e + cd /path/to/projects/my-game + cmake -S . -B build/release -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DGLFW_BUILD_WAYLAND=OFF -DGAINPUT_ENABLE_HID=OFF + cmake --build build/release --target my_game --parallel +) +``` + +The policy setting accommodates pinned legacy dependencies on CMake 4 hosts. +The GLFW setting chooses X11 on Linux; the Gainput setting avoids the optional +HID backend and its dependency download. Use the +[build reference](../development/building.md#dependency-and-toolchain-settings) +for other platform choices. No separate engine build or SDK installation is +required. To run the maintained example before writing your own entry point, +follow [First game: setup](first-game.md#setup). + +### Choose smaller target sets when needed + +The facade is convenient for a game using the configured assembly. Granular +targets let a game select fewer dependencies: + +| Link target | What the game obtains | +| --- | --- | +| `Cheryl::Cheryl` | Engine and the configured module/support assembly, including the Linux Debug terminal when selected in a source build. | +| `Cheryl::OpenGL::Startup` | Common command-line startup plus Native GLFW/OpenGL and their Engine requirements; enough for the first game's startup path. | +| `Cheryl::Engine` | Neutral Engine contracts and implementation, without selecting a native backend. | +| `Cheryl::Audio::Miniaudio` | Selected audio integration; add it when your game needs sound. | +| `Cheryl::UI::TGUI` or `Cheryl::UI::RmlUi` | The selected UI integration. | + +For example, replace the facade link with +`target_link_libraries(my_game PRIVATE Cheryl::OpenGL::Startup)` for the native +startup composition alone. The [target reference](../../README.md#targets) lists +additional support targets. Header umbrellas expose only components supplied by +the linked targets, so a narrower link can make a feature's conditional include +inactive even when its build option is enabled. + +## Use an installed SDK instead + +Choose an SDK containing Native GLFW, OpenGL, native input and Startup for the +first game. Its package must match your OS, architecture, compiler/standard-library +ABI and configuration. With an extracted SDK, replace the source-checkout block +in the game CMake file with: + +```cmake +find_package(Cheryl 4 CONFIG REQUIRED) +``` + +Keep `add_executable`, `target_link_libraries` and your asset-root definition. +Configure with the extracted SDK prefix; replace both paths below: + +```sh +( + set -e + cd /path/to/projects/my-game + cmake -S . -B build/release -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH=/absolute/path/to/extracted-sdk + cmake --build build/release --target my_game --parallel +) +``` + +This builds your game, using the libraries already in the SDK. Keep the SDK's +headers, libraries, CMake files and dependencies together. On Windows, also make +the bundled dependency DLL directories available to the process; see the +[SDK consumption contract](../development/consuming-engine.md#prebuilt-development-sdks). +The Linux Debug terminal currently requires source composition and is not exported +in SDKs. + +## Build the maintained example independently + +The first game's own CMake entry point also works outside the engine's top-level +build, without copying its files or writing another CMake project. From anywhere +inside the checkout, configure against engine source: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" + cmake -S projects/apps/first-game -B build/first-game-source -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DCHERYL_FIRST_GAME_ENGINE_SOURCE="$PWD" \ + -DGLFW_BUILD_WAYLAND=OFF -DGAINPUT_ENABLE_HID=OFF + cmake --build build/first-game-source --target first-game --parallel + ./build/first-game-source/first-game +) +``` + +Alternatively, leave the source option empty and select a compatible SDK +containing Native GLFW, native input, OpenGL, and Startup: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" + cmake -S projects/apps/first-game -B build/first-game-sdk -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DCHERYL_FIRST_GAME_ENGINE_SOURCE= \ + -DCMAKE_PREFIX_PATH=/absolute/path/to/extracted-sdk + cmake --build build/first-game-sdk --target first-game --parallel + ./build/first-game-sdk/first-game +) +``` + +The example's compiled asset path points to its source `assets` directory. Use +`--assets` when relocating those files with a packaged game. Windows executables +have an `.exe` suffix. + +## Produce engine artifacts + +The SDK is optional for source consumers. When producing engine artifacts for +others, choose the corresponding operation rather than building every target: + +| Need | Operation | Result | +| --- | --- | --- | +| Run the maintained tutorial | Select `CHERYL_BUILD_FIRST_GAME=ON`; build `first-game`. | The tutorial executable and its linked libraries. | +| Build a library directly | Build `cengine`, `module_opengl` or another concrete library target. | The selected library and dependencies in the build tree. | +| Build selected SDK libraries | Select `CHERYL_BUILD_SDK=ON`; build `cheryl-sdk`. | Compiled installable libraries; headers have not yet been collected. | +| Collect libraries and headers | After that build, install component `CherylSDK` to a prefix. | One SDK tree with headers, libraries, CMake imports and licenses. | +| Create a distributable SDK archive | Run `.github/ci/package.py` after the SDK build. | Archive, manifest and checksums; Windows also bundles its native dependencies. | +| Build the website and API reference | Build `docs` in the documentation-only project. | Generated HTML, without compiling Engine. | + +Use the [artifact commands](../development/building.md#choose-the-artifact) for +exact configuration, build, install and packaging blocks. `Cheryl::Cheryl` is an +interface joining targets; it does not produce one combined binary. + +### Keep headers and binaries matched + +A build's selections determine which module libraries, support libraries and +dependencies exist. Configuration also affects compiled logging and Debug +instrumentation. Keep separate build/prefix directories for different configurations +and module selections, and compile game translation units against the headers and +target settings that belong to those libraries. For a packaged SDK, check its +`SDK-MANIFEST.json` for the producer's source/submodule revisions, features, +toolchain and configuration. Matching filenames alone do not establish compatibility. + +Engine, Startup and the current compiled module implementations explicitly build +as static libraries. Setting `BUILD_SHARED_LIBS=ON` does not convert these targets +to shared libraries. Some third-party/native dependencies have their own linkage +choices. A shared-library distribution and its binary compatibility policy require +a separate design decision; use the current source-target or matched-SDK contract +for this walkthrough. diff --git a/docs/index.md b/docs/index.md index 3847726b..108726d5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,15 +1,16 @@ # Cheryl Engine Build a game with Cheryl's application lifecycle, input actions, retained assets, -and rendering interfaces. Follow the first-game walkthrough to assemble a small -application, then use the task guides and generated reference as you add features. +and rendering interfaces. Set up your project, then follow the first-game +walkthrough from startup and resource ownership to sprites and text on screen. +Use the task guides and generated reference as you add features. ```{toctree} :maxdepth: 2 :caption: Getting started +Project setup First game -Project setup ``` ```{toctree} @@ -72,6 +73,7 @@ Utilities :caption: Development Build +Consuming Cheryl Modules UI adapters Documentation From df0cf5129009c68c3bd564577ec49796af884d97 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 14:39:51 -0700 Subject: [PATCH 10/98] Fixes documentation workflow validation on excluded branches Moves the runner-specific documentation cache path from job-level env into a setup step using RUNNER_TEMP and GITHUB_ENV. Keeps the existing branch filters and documents why invalid workflows can fail before those filters apply. --- .github/workflows/documentation.yml | 4 +++- docs/development/documentation.md | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index a0832f78..987d92be 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -21,7 +21,6 @@ jobs: group: docs-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: - CHERYL_DOCS_CACHE_DIR: ${{ runner.temp }}/cheryl-engine/docs-tools PYTHONDONTWRITEBYTECODE: '1' defaults: run: @@ -35,6 +34,9 @@ jobs: uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.13' + - name: Select documentation cache directory + run: | + printf 'CHERYL_DOCS_CACHE_DIR=%s/cheryl-engine/docs-tools\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" - name: Check documentation tooling run: | python .github/scripts/test_sync_docs_stable.py diff --git a/docs/development/documentation.md b/docs/development/documentation.md index 4de78874..dd642f66 100644 --- a/docs/development/documentation.md +++ b/docs/development/documentation.md @@ -215,6 +215,14 @@ Actions artifacts are not the website's source and do not need an upload token f Read the Docs. Engine compilation belongs to the existing code CI; the documentation job does not compile examples. +Direct pushes to other branches do not schedule these validation jobs. Pull +requests targeting `develop` or `main` still run them. Keep workflow definitions +valid on every pushed branch: GitHub creates a failed run for each new commit +containing an invalid workflow, even on a branch excluded by its filters. Changing +branch filters or job conditions does not suppress those configuration errors; +correct the invalid YAML or expression. See +[GitHub's workflow-log guidance](https://docs.github.com/en/actions/how-tos/monitor-workflows/use-workflow-run-logs). + The Read the Docs configuration uses Ubuntu 24.04 and Python 3.13, with one custom `build.jobs.build.html` command: From e7dcbe02703006b5b5bc59d53035df9153e19747 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 14:39:51 -0700 Subject: [PATCH 11/98] Adds incremental documentation generation with explicit clean builds Caches Doxygen extraction by input and tool identity, preserves unchanged XML and generated API timestamps, reuses compatible Sphinx output, and removes obsolete pages. Invalidates interrupted or failed runs and retains serialized generation, provenance, and complete output checks. Adds --clean and docs-clean, keeps CI and Read the Docs on clean generation, documents cache behavior, and adds isolated regression fixtures to the existing CI test discovery. --- .github/workflows/documentation.yml | 2 +- .readthedocs.yaml | 2 +- cmake/CherylDocumentation.cmake | 8 +- docs/development/documentation.md | 70 ++++- docs/generation/build.py | 164 ++++++++++- docs/generation/conf.py | 3 +- docs/generation/extensions/incremental_api.py | 106 +++++++ docs/generation/tests/test_build.py | 274 ++++++++++++++++++ 8 files changed, 599 insertions(+), 30 deletions(-) create mode 100644 docs/generation/extensions/incremental_api.py create mode 100644 docs/generation/tests/test_build.py diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 987d92be..76c4588b 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -52,7 +52,7 @@ jobs: - name: Generate and check documentation run: | mkdir -p docs/generation/generated - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py 2>&1 | tee docs/generation/generated/generation.log + python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean 2>&1 | tee docs/generation/generated/generation.log - name: Retain checked HTML uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: diff --git a/.readthedocs.yaml b/.readthedocs.yaml index d7b6add0..63588806 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,4 +10,4 @@ build: jobs: build: html: - - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --output "$READTHEDOCS_OUTPUT" + - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean --output "$READTHEDOCS_OUTPUT" diff --git a/cmake/CherylDocumentation.cmake b/cmake/CherylDocumentation.cmake index 55bb9bf8..97c7c07d 100644 --- a/cmake/CherylDocumentation.cmake +++ b/cmake/CherylDocumentation.cmake @@ -28,6 +28,12 @@ function(cheryl_add_documentation_targets repository_root) COMMAND "${CHERYL_DOCS_PYTHON}" "${repository_root}/docs/generation/bootstrap.py" exec ${tool_arguments} -- python "${repository_root}/docs/generation/build.py" WORKING_DIRECTORY "${repository_root}" - COMMENT "Generating Cheryl documentation" + COMMENT "Updating Cheryl documentation" + USES_TERMINAL VERBATIM) + add_custom_target(docs-clean + COMMAND "${CHERYL_DOCS_PYTHON}" "${repository_root}/docs/generation/bootstrap.py" exec ${tool_arguments} + -- python "${repository_root}/docs/generation/build.py" --clean + WORKING_DIRECTORY "${repository_root}" + COMMENT "Regenerating Cheryl documentation from scratch" USES_TERMINAL VERBATIM) endfunction() diff --git a/docs/development/documentation.md b/docs/development/documentation.md index dd642f66..2bc23d44 100644 --- a/docs/development/documentation.md +++ b/docs/development/documentation.md @@ -9,8 +9,10 @@ The generated artifact is an HTML site rooted at `docs/generation/generated/html/index.html`, containing both the manual and API reference. For an explicit CMake artifact target, use [`docs` in the docs-only project](#cmake-entry-points); for direct generation, use -the Python command below. Enabling `CHERYL_BUILD_DOCS` makes the same target -available in an engine build; documentation is never part of the default build. +the Python command below. Both update documentation incrementally by default; +`docs-clean` or `build.py --clean` requests full regeneration. Enabling +`CHERYL_BUILD_DOCS` makes the same targets available in an engine build; +documentation is never part of the default build. ## Generate locally @@ -33,7 +35,8 @@ Open `docs/generation/generated/html/index.html`. The output directory also contains Doxygen XML, Sphinx doctrees and `sphinx-warnings.log`. `--output /path/to/output` selects another output root outside the authored documentation, or another directory beneath -`docs/generation/generated/`; its `api/` and `pycache/` directories are reserved. +`docs/generation/generated/`; its `api/`, `pycache/`, `.api-previous/` and +`.xml-previous/` directories are reserved. The generated tree is ignored by Git. Edit the declaring headers or authored guides, then regenerate. Generation is serialized within a checkout because all output profiles share the generated API source directory. @@ -72,6 +75,44 @@ to its header at the recorded source revision. Consult that header for the full declaration. Existing Mermaid fences are displayed as plain text until a diagram renderer is selected; they are not rendered diagrams. +### Incremental rebuilds and full regeneration + +The first generation performs the complete extraction and site build. Existing +output created before incremental support also needs one complete generation to +establish its cache state; the normal command handles this automatically. +Later runs reuse work from the last successful generation: + +- Editing a guide reuses Doxygen XML, unchanged API pages and Sphinx doctrees. + Sphinx rebuilds the affected pages. CSS/template changes rewrite HTML using + the cached doctrees and update the site assets. +- Editing public headers reruns Doxygen and rebuilds affected reference pages; + a changed XML file inventory starts a fresh Sphinx build. +- Changes to generation tools, configuration or the recorded source revision can + invalidate reuse and require a full rebuild. + +Each run retains the warning, link, API-inventory and source-revision checks. +A failed generation is not recorded as valid cache state; fix its cause and run +the command again. Tool-installation caches are separate from generated-site +caches and remain reusable across clean generation. + +Authored guides and included examples use Sphinx's normal modification-time +tracking. If an external tool changes their contents while preserving or +backdating timestamps, request a clean generation. + +To force full extraction and site generation: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" + python docs/generation/build.py --clean +) +``` + +`--extract-only` still limits the command to Doxygen XML; combine it with `--clean` +when you need fresh extraction without generating HTML. CI and Read the Docs use +`--clean` explicitly so publication does not depend on a previous generated site. + ## Tool selection and reuse [bootstrap.py](../generation/bootstrap.py) prefers a compatible installed @@ -111,14 +152,14 @@ coverage for CPython 3.11–3.14 on these platforms. Linux fallback has been exe Windows and macOS native setup still require execution acceptance. `status` reports tool readiness without installing anything. `setup` prepares tools -without generating documentation. Use `exec --force-pinned` for reproducible -generation, including CI and Read the Docs: +without generating documentation. `exec --force-pinned` selects the locked tools; +combine it with `--clean` for the full generation used by CI and Read the Docs: ```sh ( set -e cd "$(git rev-parse --show-toplevel)" - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py + python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean ) ``` @@ -142,9 +183,11 @@ It enables no compiler languages and does not discover engine dependencies: ) ``` -This writes `docs/generation/generated/html/index.html`. The optional `docs-tools` -target prepares tools; the `docs` target also selects them, so running `docs-tools` -first is unnecessary. Neither target belongs to the default build. +This incrementally updates `docs/generation/generated/html/index.html`. Select +`docs-clean` instead of `docs` in the build command to force full regeneration. +The optional `docs-tools` target prepares tools; both generation targets also +select them, so running `docs-tools` first is unnecessary. None of these targets +belongs to the default build. To add documentation to an existing root engine build, reconfigure that same build directory and explicitly select the target: @@ -161,8 +204,8 @@ directory and explicitly select the target: Use your existing build directory in place of `build/debug`; its other cached settings are retained. The root project still has its usual compiler and dependency requirements; use the [building guide](building.md) for an initial engine -configuration. Building `docs` runs documentation generation without compiling the -engine. Both CMake entry points expose +configuration. Building `docs` or `docs-clean` runs documentation generation +without compiling the engine. Both CMake entry points expose `CHERYL_DOCS_PYTHON`, `CHERYL_DOCS_CACHE_DIR` and `CHERYL_DOCS_FORCE_PINNED`: ```sh @@ -211,6 +254,7 @@ GitHub Actions generates and checks HTML for pull requests, pushes to `develop` and `main`, `v*` tags, and manual runs. It retains checked HTML and build diagnostics as downloadable artifacts for 30 days. Read the Docs independently clones the selected Git ref, runs the same pinned generator, and hosts the resulting HTML. +Both publication paths request full generation with `--clean`. Actions artifacts are not the website's source and do not need an upload token for Read the Docs. Engine compilation belongs to the existing code CI; the documentation job does not compile examples. @@ -235,7 +279,7 @@ build: jobs: build: html: - - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --output "$READTHEDOCS_OUTPUT" + - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean --output "$READTHEDOCS_OUTPUT" ``` Keep `sphinx`, `mkdocs` and a separate Python installation step out of this custom @@ -300,7 +344,7 @@ pass before a change reaches `main`. ### Publication verification and recovery -Every full build writes `html/revision.json`. Its +Every successful HTML generation writes `html/revision.json`. Its `source_revision` is the full checked Git commit, `source_ref` identifies the build ref, and `dirty` records whether the source checkout differs from that commit. The marker also records selected tool versions and lock/manifest hashes, without diff --git a/docs/generation/build.py b/docs/generation/build.py index f72eba20..5f096a9a 100644 --- a/docs/generation/build.py +++ b/docs/generation/build.py @@ -23,7 +23,7 @@ sys.pycache_prefix = str(GENERATED_ROOT / "pycache") os.environ["PYTHONPYCACHEPREFIX"] = sys.pycache_prefix -from bootstrap import file_lock, validate_doxygen +from bootstrap import atomic_json, file_lock, read_json, validate_doxygen PUBLIC_INCLUDE_ROOTS = tuple( @@ -43,6 +43,7 @@ ) ) HEADER_SUFFIXES = {".h", ".hpp", ".inl", ".ipp", ".tpp"} +CACHE_FORMAT = 1 def header_inputs() -> list[Path]: @@ -124,7 +125,7 @@ def provenance(commit: str, doxygen_version: str, output: Path) -> dict: } -def extract(output: Path, doxygen: str, commit: str) -> None: +def doxygen_configuration(output: Path, commit: str) -> str: headers = header_inputs() template = Path(__file__).with_name("Doxyfile.in").read_text(encoding="utf-8") replacements = { @@ -136,6 +137,94 @@ def extract(output: Path, doxygen: str, commit: str) -> None: } for key, value in replacements.items(): template = template.replace(f"@{key}@", value) + return template + + +def file_hashes(directory: Path, pattern: str = "*") -> dict[str, str]: + return { + path.relative_to(directory).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() + for path in sorted(directory.rglob(pattern)) + if path.is_file() + } + + +def fingerprint(value: dict) -> str: + return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest() + + +def extraction_fingerprint(output: Path, doxygen: str, commit: str) -> str: + return fingerprint({ + "format": CACHE_FORMAT, + "configuration": doxygen_configuration(output, commit), + "headers": { + path.relative_to(REPO_ROOT).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() + for path in header_inputs() + }, + "extractor": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + "doxygen": str(Path(doxygen).resolve()), + "doxygen_sha256": hashlib.sha256(Path(doxygen).read_bytes()).hexdigest(), + }) + + +def rendering_fingerprint(output: Path, metadata: dict, xml_files: dict[str, str]) -> str: + scripts = { + path.relative_to(GENERATION_ROOT).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() + for path in sorted([*GENERATION_ROOT.glob("*.py"), *(GENERATION_ROOT / "extensions").glob("*.py")]) + } + return fingerprint({ + "format": CACHE_FORMAT, + "repository": str(REPO_ROOT), + "output": str(output), + "revision": metadata["source_revision"], + "tools": metadata["tools"], + "python": str(Path(sys.executable).resolve()), + "requirements": metadata["requirements_sha256"], + "native_manifest": metadata["native_manifest_sha256"], + "scripts": scripts, + # Breathe records XML dependencies and cannot stat a removed compound. + "xml_files": sorted(xml_files), + "readthedocs_version": os.environ.get("READTHEDOCS_VERSION_NAME"), + }) + + +def presentation_fingerprint(metadata: dict) -> str: + return fingerprint({ + "templates": file_hashes(GENERATION_ROOT / "templates"), + "static": file_hashes(GENERATION_ROOT / "static"), + "dirty": metadata["dirty"], + }) + + +def refresh_xml(output: Path, doxygen: str, commit: str, *, incremental: bool) -> None: + xml = output / "xml" + previous = output / ".xml-previous" + # Recover an interrupted extraction before staging its replacement. + if previous.exists(): + if xml.exists(): + shutil.rmtree(xml) + previous.replace(xml) + if xml.exists(): + xml.replace(previous) + try: + extract(output, doxygen, commit) + if incremental and previous.exists(): + for path in xml.rglob("*"): + original = previous / path.relative_to(xml) + if path.is_file() and original.is_file() and path.read_bytes() == original.read_bytes(): + status = original.stat() + os.utime(path, ns=(status.st_atime_ns, status.st_mtime_ns)) + except BaseException: + if xml.exists(): + shutil.rmtree(xml) + if previous.exists(): + previous.replace(xml) + raise + if previous.exists(): + shutil.rmtree(previous) + + +def extract(output: Path, doxygen: str, commit: str) -> None: + template = doxygen_configuration(output, commit) output.mkdir(parents=True, exist_ok=True) configuration = output / "Doxyfile" configuration.write_text(template, encoding="utf-8") @@ -149,10 +238,10 @@ def extract(output: Path, doxygen: str, commit: str) -> None: # from the public index and relationship navigation before Exhale reads it. index_path = output / "xml/index.xml" index = ElementTree.parse(index_path) - doxygen_configuration = ElementTree.parse(output / "xml/Doxyfile.xml") + configuration_xml = ElementTree.parse(output / "xml/Doxyfile.xml") private_namespaces = { value.text[:-3] - for value in doxygen_configuration.findall("option[@id='EXCLUDE_SYMBOLS']/value") + for value in configuration_xml.findall("option[@id='EXCLUDE_SYMBOLS']/value") if value.text and value.text.endswith("::*") } declarations = {} @@ -232,6 +321,9 @@ def main() -> int: parser.add_argument( "--extract-only", action="store_true", help="Generate Doxygen XML only." ) + parser.add_argument( + "--clean", action="store_true", help="Regenerate XML, API pages and HTML without reusing build caches." + ) parser.add_argument("--doxygen", help="Use this Doxygen executable explicitly.") arguments = parser.parse_args() if arguments.doxygen is not None and os.environ.get("CHERYL_DOCS_FORCE_PINNED") == "1": @@ -255,6 +347,8 @@ def main() -> int: or (REPO_ROOT / "docs").is_relative_to(output) or any(root.is_relative_to(output) for root in PUBLIC_INCLUDE_ROOTS) or output.is_relative_to(GENERATED_ROOT / "api") + or output.is_relative_to(GENERATED_ROOT / ".api-previous") + or output.is_relative_to(GENERATED_ROOT / ".xml-previous") or output.is_relative_to(GENERATED_ROOT / "pycache")): parser.error("The output root must not overwrite authored sources, generated API pages or Python caches.") doxygen = arguments.doxygen or os.environ.get("CHERYL_DOCS_DOXYGEN") or shutil.which( @@ -272,23 +366,64 @@ def main() -> int: if not doxygen: parser.error(f"Doxygen executable not found: {arguments.doxygen}") doxygen_version = validate_doxygen(Path(doxygen)) - commit = revision() # Every output/profile uses this same lock because Exhale writes source pages. with file_lock(GENERATED_ROOT / ".generation.lock"): - extract(output, doxygen, commit) + commit = revision() + output.mkdir(parents=True, exist_ok=True) + state_path = output / "build-state.json" + state = read_json(state_path) + if not isinstance(state, dict) or state.get("format") != CACHE_FORMAT: + state = {} + extract_key = extraction_fingerprint(output, doxygen, commit) + xml_files = file_hashes(output / "xml") + reuse_xml = ( + not arguments.clean + and state.get("extraction") == extract_key + and bool(xml_files) + and state.get("xml") == xml_files + and (output / "xml/index.xml").is_file() + and not (output / ".xml-previous").exists() + ) + # Sphinx can save doctrees before failing on warnings. Never reuse an + # interrupted/failed run as a successful baseline on the next invocation. + state_path.unlink(missing_ok=True) + if reuse_xml: + print("Reusing cached Doxygen XML.", flush=True) + else: + print("Extracting public headers with Doxygen.", flush=True) + refresh_xml(output, doxygen, commit, incremental=not arguments.clean) + xml_files = file_hashes(output / "xml") + next_state = {"format": CACHE_FORMAT, "extraction": extract_key, "xml": xml_files} if arguments.extract_only: + atomic_json(state_path, next_state) return 0 - generated = GENERATED_ROOT / "api" - if generated.exists(): - shutil.rmtree(generated) - for directory in (output / "html", output / "doctrees"): - if directory.exists(): - shutil.rmtree(directory) + + metadata = provenance(commit, doxygen_version, output) + render_key = rendering_fingerprint(output, metadata, xml_files) + presentation_key = presentation_fingerprint(metadata) + fresh = ( + arguments.clean + or state.get("rendering") != render_key + or not (output / "doctrees/environment.pickle").is_file() + or not (output / "html/index.html").is_file() + ) + if fresh: + print("Generating fresh Sphinx output.", flush=True) + for directory in (GENERATED_ROOT / "api", GENERATED_ROOT / ".api-previous", + output / "html", output / "doctrees"): + if directory.exists(): + shutil.rmtree(directory) + else: + print("Updating Sphinx output incrementally.", flush=True) + environment = os.environ.copy() environment["CHERYL_DOCS_XML_DIR"] = str(output / "xml") environment["CHERYL_DOCS_REVISION"] = commit - metadata = provenance(commit, doxygen_version, output) + environment["CHERYL_DOCS_INCREMENTAL"] = "0" if arguments.clean else "1" environment["CHERYL_DOCS_DIRTY"] = "1" if metadata["dirty"] else "0" + # CSS/template changes and the dirty-source banner affect every HTML + # page, but do not require reparsing unchanged source documents. + write_all = ["-a"] if not fresh and state.get("presentation") != presentation_key else [] subprocess.run( [ sys.executable, @@ -301,6 +436,7 @@ def main() -> int: "-W", "--keep-going", "-n", + *write_all, "-w", str(output / "sphinx-warnings.log"), "-d", @@ -320,6 +456,8 @@ def main() -> int: "--html", str(output / "html"), "--xml", str(output / "xml")], cwd=REPO_ROOT, check=True, ) + next_state.update(rendering=render_key, presentation=presentation_key) + atomic_json(state_path, next_state) return 0 diff --git a/docs/generation/conf.py b/docs/generation/conf.py index 517eb184..d4425b43 100644 --- a/docs/generation/conf.py +++ b/docs/generation/conf.py @@ -28,7 +28,7 @@ source_repository = "https://github.com/cppcooper/cheryl-engine" source_repository_root = str(REPO_ROOT) -extensions = ["myst_parser", "breathe", "exhale", "api_rendering", "repository_links", "inline_api"] +extensions = ["myst_parser", "breathe", "exhale", "api_rendering", "repository_links", "inline_api", "incremental_api"] source_suffix = {".md": "markdown", ".rst": "restructuredtext"} root_doc = "index" exclude_patterns = [ @@ -41,6 +41,7 @@ # Ignore generated pages left behind by older checkouts. "generated/**", "api/generated/**", + "generation/generated/.api-previous/**", "**/.DS_Store", ] # Only generated API pages participate in source discovery inside this area. diff --git a/docs/generation/extensions/incremental_api.py b/docs/generation/extensions/incremental_api.py new file mode 100644 index 00000000..a11226a3 --- /dev/null +++ b/docs/generation/extensions/incremental_api.py @@ -0,0 +1,106 @@ +"""Preserve unchanged API sources and retire removed HTML during local rebuilds.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +import os +from pathlib import Path +import shutil + +from sphinx.errors import ExtensionError +from sphinx.util import logging + + +LOGGER = logging.getLogger(__name__) + + +@dataclass +class Generation: + current: Path + previous: Path + removed: set[str] = field(default_factory=set) + + +def restore(generation: Generation) -> None: + if generation.previous.is_dir(): + if generation.current.exists(): + shutil.rmtree(generation.current) + generation.previous.replace(generation.current) + + +def begin(app) -> None: + current = Path(app.srcdir) / "generation/generated/api" + generation = Generation(current, current.with_name(".api-previous")) + app._cheryl_api_generation = generation + if os.environ.get("CHERYL_DOCS_INCREMENTAL") != "1": + if generation.previous.exists(): + shutil.rmtree(generation.previous) + return + + # A stopped process may not have reached build-finished. Recover its complete + # sources before staging again; the wrapper serializes generation with a lock. + restore(generation) + if current.exists(): + current.replace(generation.previous) + + +def preserve(app) -> None: + generation = app._cheryl_api_generation + if not generation.previous.is_dir(): + return + + unchanged = 0 + for current in generation.current.rglob("*"): + if not current.is_file(): + continue + previous = generation.previous / current.relative_to(generation.current) + if not previous.is_file(): + continue + metadata = previous.stat() + if current.stat().st_size == metadata.st_size and current.read_bytes() == previous.read_bytes(): + os.utime(current, ns=(metadata.st_atime_ns, metadata.st_mtime_ns)) + unchanged += 1 + LOGGER.info("API sources: preserved timestamps for %d unchanged files", unchanged) + + +def remember_removed(app, environment, added, changed, removed): + app._cheryl_api_generation.removed.update(removed) + return [] + + +def remove_output(path: Path, output: Path) -> None: + if not path.resolve().is_relative_to(output): + raise ExtensionError(f"Removed documentation output escapes the HTML directory: {path}") + path.unlink(missing_ok=True) + + +def finish(app, exception) -> None: + generation = getattr(app, "_cheryl_api_generation", None) + if generation is None: + return + if exception is not None: + restore(generation) + return + + if app.builder.name == "html": + output = Path(app.outdir).resolve() + for document in generation.removed: + remove_output(Path(app.builder.get_outfilename(document)), output) + for suffix in app.config.source_suffix: + source = document + suffix + if suffix != app.config.html_sourcelink_suffix: + source += app.config.html_sourcelink_suffix + remove_output(output / "_sources" / source, output) + + if generation.previous.exists(): + shutil.rmtree(generation.previous) + + +def setup(app) -> dict[str, object]: + # Exhale and the metadata extensions run at the default priority of 500. + # Compare only after every generator has finished, before Sphinx reads files. + app.connect("builder-inited", begin, priority=100) + app.connect("builder-inited", preserve, priority=900) + app.connect("env-get-outdated", remember_removed) + app.connect("build-finished", finish) + return {"version": "1", "parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/generation/tests/test_build.py b/docs/generation/tests/test_build.py new file mode 100644 index 00000000..5480fd46 --- /dev/null +++ b/docs/generation/tests/test_build.py @@ -0,0 +1,274 @@ +"""Incremental build regressions with temporary outputs and simulated tools.""" + +from __future__ import annotations + +from contextlib import nullcontext, redirect_stderr, redirect_stdout +import importlib.util +import io +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + + +TOOLS_ROOT = Path(__file__).resolve().parents[1] +with tempfile.TemporaryDirectory(prefix="cheryl-docs-build-import-") as cache: + # Importing the wrapper must not put bytecode in a real documentation tree. + with patch.object(sys, "pycache_prefix", str(Path(cache) / "pycache")), patch.dict( + os.environ, {"PYTHONPYCACHEPREFIX": str(Path(cache) / "pycache")} + ): + sys.path.insert(0, str(TOOLS_ROOT)) + try: + spec = importlib.util.spec_from_file_location("cheryl_docs_build", TOOLS_ROOT / "build.py") + builder = importlib.util.module_from_spec(spec) + spec.loader.exec_module(builder) + finally: + sys.path.pop(0) + + +class BuildTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory(prefix="cheryl-docs-build-") + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name).resolve() / "checkout" + self.tools = self.root / "docs/generation" + self.output = self.tools / "generated" + self.include = self.root / "projects/engine/include" + self.header = self.include / "cheryl/fixture.h" + self.header.parent.mkdir(parents=True) + self.header.write_text("#pragma once\nstruct Fixture {};\n", encoding="utf-8") + self.tools.mkdir(parents=True) + for filename in ("build.py", "Doxyfile.in"): + (self.tools / filename).write_bytes((TOOLS_ROOT / filename).read_bytes()) + (self.tools / "check.py").write_text("# Simulated output checker\n", encoding="utf-8") + (self.tools / "extensions").mkdir() + (self.tools / "extensions/fixture.py").write_text("# Fixture extension\n", encoding="utf-8") + (self.tools / "templates").mkdir() + (self.tools / "templates/layout.html").write_text("{{ body }}\n", encoding="utf-8") + (self.tools / "static").mkdir() + self.stylesheet = self.tools / "static/site.css" + self.stylesheet.write_text("body { font-size: 100%; }\n", encoding="utf-8") + self.guide = self.root / "docs/guide.md" + self.guide.write_text("# Guide\nInitial instructions.\n", encoding="utf-8") + self.doxygen = self.root / "tools/doxygen" + self.doxygen.parent.mkdir() + self.doxygen.write_bytes(b"simulated Doxygen executable") + self.state = self.output / "build-state.json" + self.html_marker = self.output / "html/retained.html" + self.doctree_marker = self.output / "doctrees/retained.doctree" + self.documents = { + "index.xml": '' + "Fixture\n", + "classFixture.xml": '' + "Fixture\n", + } + self.metadata = { + "source_revision": "1" * 40, + "dirty": False, + "tools": {"python": "fixture", "python_source": "system", "packages": {"Sphinx": "8.2.3"}}, + "requirements_sha256": "fixture requirements", + "native_manifest_sha256": "fixture native tools", + } + self.sphinx_runs = [] + self.check_runs = [] + self.fail_sphinx = False + self.enterContext(patch.dict(os.environ, { + "CHERYL_DOCS_TOOLS_ACTIVE": "1", + "CHERYL_DOCS_DOXYGEN": str(self.doxygen), + }, clear=True)) + for name, value in ( + ("REPO_ROOT", self.root), + ("GENERATION_ROOT", self.tools), + ("GENERATED_ROOT", self.output), + ("PUBLIC_INCLUDE_ROOTS", (self.include,)), + ("__file__", str(self.tools / "build.py")), + ): + self.enterContext(patch.object(builder, name, value)) + self.enterContext(patch.object(builder, "header_inputs", return_value=[self.header])) + self.enterContext(patch.object(builder, "revision", return_value=self.metadata["source_revision"])) + self.enterContext(patch.object(builder, "provenance", return_value=self.metadata)) + self.enterContext(patch.object(builder, "validate_doxygen", return_value="1.18.0")) + self.enterContext(patch.object(builder, "file_lock", side_effect=lambda path: nullcontext())) + self.extractor = self.enterContext(patch.object(builder, "extract", side_effect=self.extract)) + self.enterContext(patch.object(builder.subprocess, "run", side_effect=self.run_tool)) + + def extract(self, output, doxygen, commit): + xml = output / "xml" + xml.mkdir(parents=True, exist_ok=True) + for name, contents in self.documents.items(): + (xml / name).write_text(contents, encoding="utf-8") + + def run_tool(self, command, **options): + if command[1:3] == ["-m", "sphinx"]: + output = Path(command[-1]) + doctrees = Path(command[command.index("-d") + 1]) + environment = doctrees / "environment.pickle" + self.sphinx_runs.append({ + "command": command, + "environment": environment.read_bytes() if environment.exists() else None, + "html_retained": self.html_marker.exists(), + "doctrees_retained": self.doctree_marker.exists(), + "incremental": options["env"]["CHERYL_DOCS_INCREMENTAL"], + }) + output.mkdir(parents=True, exist_ok=True) + doctrees.mkdir(parents=True, exist_ok=True) + (output / "index.html").write_text("Fixture\n", encoding="utf-8") + environment.write_text(f"environment-{len(self.sphinx_runs)}", encoding="utf-8") + if self.fail_sphinx: + self.fail_sphinx = False + # A warning failure can leave apparently complete cached output. + raise subprocess.CalledProcessError(1, command) + elif len(command) > 1 and Path(command[1]) == self.tools / "check.py": + self.check_runs.append(command) + else: + raise AssertionError(f"Unexpected external tool: {command!r}") + return subprocess.CompletedProcess(command, 0) + + def run_build(self, *arguments): + with patch.object(sys, "argv", [str(self.tools / "build.py"), *arguments]), \ + redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()): + return builder.main() + + def retain_markers(self): + self.html_marker.write_text("Existing HTML\n", encoding="utf-8") + self.doctree_marker.write_text("Existing doctree\n", encoding="utf-8") + + def test_warm_reuse(self): + self.assertEqual(self.run_build(), 0) + self.retain_markers() + self.guide.write_text("# Guide\nRevised instructions.\n", encoding="utf-8") + + self.assertEqual(self.run_build(), 0) + + self.assertEqual(self.extractor.call_count, 1) + self.assertEqual(self.sphinx_runs[-1]["environment"], b"environment-1") + self.assertTrue(self.sphinx_runs[-1]["html_retained"]) + self.assertTrue(self.sphinx_runs[-1]["doctrees_retained"]) + self.assertNotIn("-a", self.sphinx_runs[-1]["command"]) + self.assertEqual(len(self.check_runs), 2) + self.assertTrue(self.state.is_file()) + + def test_header_change(self): + self.run_build() + self.header.write_text("#pragma once\nstruct Fixture { int value; };\n", encoding="utf-8") + + self.run_build() + + self.assertEqual(self.extractor.call_count, 2) + self.assertEqual(len(self.check_runs), 2) + + def test_removed_compound(self): + original_index = self.documents["index.xml"] + self.documents["index.xml"] = original_index.replace( + "", + 'Obsolete', + ) + self.documents["classObsolete.xml"] = '\n' + self.run_build() + self.retain_markers() + self.documents["index.xml"] = original_index + self.documents.pop("classObsolete.xml") + self.header.write_text("#pragma once\nstruct Fixture { int replacement; };\n", encoding="utf-8") + + self.run_build() + + self.assertEqual(self.extractor.call_count, 2) + self.assertIsNone(self.sphinx_runs[-1]["environment"]) + self.assertFalse(self.sphinx_runs[-1]["html_retained"]) + self.assertFalse(self.sphinx_runs[-1]["doctrees_retained"]) + self.assertFalse((self.output / "xml/classObsolete.xml").exists()) + + def test_css_rewrites(self): + self.run_build() + self.retain_markers() + self.stylesheet.write_text("body { font-size: 120%; }\n", encoding="utf-8") + + self.run_build() + + self.assertEqual(self.extractor.call_count, 1) + self.assertIn("-a", self.sphinx_runs[-1]["command"]) + self.assertEqual(self.sphinx_runs[-1]["environment"], b"environment-1") + self.assertTrue(self.sphinx_runs[-1]["html_retained"]) + self.assertTrue(self.sphinx_runs[-1]["doctrees_retained"]) + + def test_failed_run_retries(self): + self.run_build() + self.retain_markers() + self.fail_sphinx = True + with self.assertRaises(subprocess.CalledProcessError): + self.run_build() + self.assertFalse(self.state.exists()) + self.assertEqual((self.output / "doctrees/environment.pickle").read_bytes(), b"environment-2") + + self.run_build() + + self.assertEqual(self.extractor.call_count, 2) + self.assertIsNone(self.sphinx_runs[-1]["environment"]) + self.assertFalse(self.sphinx_runs[-1]["html_retained"]) + self.assertFalse(self.sphinx_runs[-1]["doctrees_retained"]) + self.assertEqual(len(self.check_runs), 2) + self.assertTrue(self.state.is_file()) + + def test_clean(self): + self.run_build() + self.retain_markers() + + self.run_build("--clean") + + self.assertEqual(self.extractor.call_count, 2) + self.assertIsNone(self.sphinx_runs[-1]["environment"]) + self.assertFalse(self.sphinx_runs[-1]["html_retained"]) + self.assertFalse(self.sphinx_runs[-1]["doctrees_retained"]) + self.assertEqual(self.sphinx_runs[-1]["incremental"], "0") + + def test_xml_preserves_unchanged(self): + self.documents["obsolete.xml"] = "\n" + self.run_build() + index = self.output / "xml/index.xml" + os.utime(index, ns=(1_600_000_000_000_000_000, 1_600_000_000_000_000_000)) + original_mtime = index.stat().st_mtime_ns + self.documents.pop("obsolete.xml") + self.documents["classFixture.xml"] = '\n' + + builder.refresh_xml(self.output, str(self.doxygen), self.metadata["source_revision"], incremental=True) + + self.assertEqual(index.stat().st_mtime_ns, original_mtime) + self.assertEqual((self.output / "xml/classFixture.xml").read_text(encoding="utf-8"), + self.documents["classFixture.xml"]) + self.assertFalse((self.output / "xml/obsolete.xml").exists()) + self.assertFalse((self.output / ".xml-previous").exists()) + + def test_xml_failure_restores(self): + self.run_build() + original = {path.name: path.read_bytes() for path in (self.output / "xml").iterdir()} + + def fail(output, doxygen, commit): + xml = output / "xml" + xml.mkdir() + (xml / "index.xml").write_text("partial output", encoding="utf-8") + raise RuntimeError("Extraction interrupted") + + self.extractor.side_effect = fail + with self.assertRaisesRegex(RuntimeError, "Extraction interrupted"): + builder.refresh_xml(self.output, str(self.doxygen), self.metadata["source_revision"], incremental=True) + + self.assertEqual({path.name: path.read_bytes() for path in (self.output / "xml").iterdir()}, original) + self.assertFalse((self.output / ".xml-previous").exists()) + + def test_reserved_outputs(self): + for name in ("api", ".api-previous", ".xml-previous", "pycache"): + for relative in (name, name + "/nested"): + with self.subTest(output=relative): + with self.assertRaises(SystemExit) as error: + self.run_build("--output", str(self.output / relative)) + self.assertEqual(error.exception.code, 2) + self.assertFalse((self.output / relative).exists()) + self.extractor.assert_not_called() + self.assertEqual(self.sphinx_runs, []) + + +if __name__ == "__main__": + unittest.main() From 79807d1311150a2923aa5ae3e9fe4e2a98ecd6bd Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 14:46:15 -0700 Subject: [PATCH 12/98] Adds a root documentation generation script Provides an executable Bash entry point for incremental, clean and extraction-only documentation builds. Preserves caller-relative paths and forwards options through the Python generator, including pinned tools and shared cache selection. Documents shell, Python and CMake entry points and keeps the documentation artifact target lists current. --- README.md | 8 ++++- docs/development/building.md | 16 ++++++---- docs/development/documentation.md | 52 +++++++++++++++++++++---------- docs/generation/build.py | 20 ++++++++++-- generate-docs.sh | 17 ++++++++++ 5 files changed, 87 insertions(+), 26 deletions(-) create mode 100755 generate-docs.sh diff --git a/README.md b/README.md index 72214f7d..4d45f4c7 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ the public aliases to inherit headers and dependencies. | `module_audio_miniaudio` | `Cheryl::Audio::Miniaudio` | Audio decoding, mixing, native output and streaming. | | `demo` | — | Example native application. | | `first-game` | — | Opt-in maintained game from the [walkthrough](docs/getting-started/first-game.md), with its own assets. | -| `docs`, `docs-tools` | — | Optional documentation generation and shared tool setup; also available through the docs-only CMake project. | +| `docs`, `docs-clean`, `docs-tools` | — | Optional incremental generation, full regeneration and shared documentation tool setup; also available through the docs-only CMake project. | | `cheryl-sdk` | — | Builds selected installable libraries when `CHERYL_BUILD_SDK=ON`. | See the [module index](projects/modules/README.md) for each owner's API and the @@ -146,6 +146,12 @@ for the steps required to add an owner. ## Documentation and development +From the checkout root, run `./generate-docs.sh` to update the local manual and API +reference. Add `--clean` for full regeneration, `--extract-only` for Doxygen XML, +or `--help` for output and tool-selection options. The +[generation guide](docs/development/documentation.md) also covers Python and CMake +entry points and hosted publication. + | Topic | Documentation | |----------------------------------------| --- | | Complete documentation index | [All guides and system references](docs/README.md) | diff --git a/docs/development/building.md b/docs/development/building.md index 0a67e170..b11c4dcc 100644 --- a/docs/development/building.md +++ b/docs/development/building.md @@ -15,7 +15,7 @@ source directories. SDK installation collects them into one redistributable | Artifact | Trigger | Result | | --- | --- | --- | -| HTML manual and C++ reference | Build `docs` in the docs-only project below, or enable `CHERYL_BUILD_DOCS` in an engine build. | `docs/generation/generated/html/index.html` and linked pages. | +| HTML manual and C++ reference | Run `./generate-docs.sh`, or build `docs` in the docs-only project below; `--clean` / `docs-clean` forces full regeneration. Enable `CHERYL_BUILD_DOCS` to add these targets to an engine build. | `docs/generation/generated/html/index.html` and linked pages. | | Engine library | Build `cengine`; see [engine-only configuration](#engine-only-configuration). | `libcheryl-engine.a` on Linux or `cheryl-engine.lib` on Windows in the build root, plus required dependency artifacts. | | Selected SDK libraries | Enable `CHERYL_BUILD_SDK`, then build `cheryl-sdk`. | Engine, selected modules/support and their owned dependencies in the build tree. | | Redistributable SDK directory | Build `cheryl-sdk`, then install the `CherylSDK` component. | One prefix containing `include/`, libraries, CMake package files and licenses. | @@ -32,8 +32,11 @@ archive. See ### Build documentation -This uses no C/C++ compiler or engine dependencies. Run from any directory inside -the checkout; the subshell preserves your starting directory: +Run `./generate-docs.sh` from the checkout root for direct generation, or add +`--clean` for full regeneration. The documentation-only CMake project provides +the same operations through `docs` and `docs-clean`. Neither path needs a C/C++ +compiler or engine dependencies. Run this block from any directory inside the +checkout; the subshell preserves your starting directory: ```sh ( @@ -47,8 +50,9 @@ the checkout; the subshell preserves your starting directory: Open `docs/generation/generated/html/index.html`. Python 3.11 or newer is required; the generator selects compatible installed tools or prepares pinned tools in the user cache. Initial tool setup may need network access. See -[documentation generation](documentation.md) for direct Python invocation, tool -selection and output options. The `docs` target prepares its tools automatically. +[documentation generation](documentation.md) for shell/Python entry points, tool +selection and output options. The `docs` and `docs-clean` targets prepare their +tools automatically. ## Configuration options @@ -70,7 +74,7 @@ declared only when RmlUi is selected. | `CHERYL_BUILD_AUDIO_MINIAUDIO` | `OFF` | Selects miniaudio decoding, output and streaming. | | `CHERYL_BUILD_DEMO` | `ON` | Adds `demo` with Native GLFW, OpenGL and native input selected. | | `CHERYL_BUILD_FIRST_GAME` | `OFF` | Adds the maintained `first-game` walkthrough application with Native GLFW, OpenGL and native input selected. | -| `CHERYL_BUILD_DOCS` | `OFF` | Adds the optional `docs-tools` and `docs` targets; tools are selected when these targets run. | +| `CHERYL_BUILD_DOCS` | `OFF` | Adds the optional `docs-tools`, `docs` and `docs-clean` targets; tools are selected when these targets run. | | `CHERYL_BUILD_TESTS` | `ON` | Adds focused tests and explicitly buildable acceptance/aggregate runners. | | `CHERYL_BUILD_ALL_TESTS` | `OFF` | Includes aggregates in the default build and CTest; requires tests. | | `CHERYL_BUILD_ACCEPTANCE_TESTS` | `OFF` | Includes acceptance runners in the default build; requires tests. | diff --git a/docs/development/documentation.md b/docs/development/documentation.md index 2bc23d44..5e98d986 100644 --- a/docs/development/documentation.md +++ b/docs/development/documentation.md @@ -8,9 +8,10 @@ or compile the engine. The generated artifact is an HTML site rooted at `docs/generation/generated/html/index.html`, containing both the manual and API reference. For an explicit CMake artifact target, use -[`docs` in the docs-only project](#cmake-entry-points); for direct generation, use -the Python command below. Both update documentation incrementally by default; -`docs-clean` or `build.py --clean` requests full regeneration. Enabling +[`docs` in the docs-only project](#cmake-entry-points); the repository-root +[`generate-docs.sh`](../../generate-docs.sh) script provides the direct command. +Both update documentation incrementally by default; `docs-clean` or +`./generate-docs.sh --clean` requests full regeneration. Enabling `CHERYL_BUILD_DOCS` makes the same targets available in an engine build; documentation is never part of the default build. @@ -19,15 +20,16 @@ documentation is never part of the default build. Use a Git checkout and Python 3.11 or newer with the standard `venv` module. You do not need to install Sphinx, Doxygen or pip manually. The first fallback setup needs network access to PyPI and the official Doxygen release assets, and a writable -user cache. Use `python3` in the commands below if that is your interpreter's name. -Run the shell blocks from any directory inside the checkout; the subshell locates -the repository root and preserves your starting directory. +user cache. The shell entry point requires Bash and selects `python3`, falling +back to `python`; set `CHERYL_DOCS_PYTHON` to choose another interpreter. Run this +block from any directory inside the checkout; the subshell preserves your starting +directory: ```sh ( set -e cd "$(git rev-parse --show-toplevel)" - python docs/generation/build.py + ./generate-docs.sh ) ``` @@ -41,6 +43,20 @@ The generated tree is ignored by Git. Edit the declaring headers or authored guides, then regenerate. Generation is serialized within a checkout because all output profiles share the generated API source directory. +The script locates the repository independently of the current directory. You +can also invoke `/path/to/cheryl-engine/generate-docs.sh` from elsewhere. It +preserves that working directory, so relative `--output` and `--cache-dir` paths +resolve from where you invoke it. Use `--help` for the available generation and +tool-selection options. `--extract-only` generates only Doxygen XML; `--output` +and `--doxygen` select an output root and explicit Doxygen executable. + +The Python entry point remains supported, including on Windows without Bash: +invoke `python docs/generation/build.py` from the checkout root, or use its +absolute path from another directory. It accepts the same generation flags and +selects tools through bootstrap. Tool-only `status` and `setup` operations remain +available through `docs/generation/bootstrap.py`; the shell script handles +documentation generation. + The generation scripts, Sphinx configuration, extensions, styles, templates and dependency manifests live under `docs/generation/`. Sphinx reads the authored pages from `docs/` using that separate configuration directory. Generated API @@ -105,7 +121,7 @@ To force full extraction and site generation: ( set -e cd "$(git rev-parse --show-toplevel)" - python docs/generation/build.py --clean + ./generate-docs.sh --clean ) ``` @@ -151,23 +167,25 @@ also requires matching binary wheels. The pinned Python lock has wheel metadata coverage for CPython 3.11–3.14 on these platforms. Linux fallback has been exercised; Windows and macOS native setup still require execution acceptance. -`status` reports tool readiness without installing anything. `setup` prepares tools -without generating documentation. `exec --force-pinned` selects the locked tools; -combine it with `--clean` for the full generation used by CI and Read the Docs: +Bootstrap's `status` reports tool readiness without installing anything; `setup` +prepares tools without generating documentation. The shell script's +`--force-pinned` selects the locked tools. Combine it with `--clean` for the full +generation used by CI and Read the Docs: ```sh ( set -e cd "$(git rev-parse --show-toplevel)" - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean + ./generate-docs.sh --force-pinned --clean ) ``` -Set `CHERYL_DOCS_CACHE_DIR` or pass bootstrap `--cache-dir /path/to/cache` to choose -another shared cache. To select another interpreter, invoke bootstrap with that -interpreter. `build.py --doxygen /path/to/doxygen` supplies an explicit executable, -which must meet the minimum and takes precedence over automatic native tool -selection. Keep local overrides out of publication configuration. +Set `CHERYL_DOCS_CACHE_DIR` or pass `./generate-docs.sh --cache-dir /path/to/cache` +to choose another shared cache. `CHERYL_DOCS_PYTHON=/path/to/python` selects the +script's interpreter; the Python and CMake entry points also support explicit +interpreter selection. `./generate-docs.sh --doxygen /path/to/doxygen` supplies an +executable that must meet the minimum and takes precedence over automatic native +tool selection. Keep local overrides out of publication configuration. ## CMake entry points diff --git a/docs/generation/build.py b/docs/generation/build.py index 5f096a9a..30589dac 100644 --- a/docs/generation/build.py +++ b/docs/generation/build.py @@ -324,12 +324,28 @@ def main() -> int: parser.add_argument( "--clean", action="store_true", help="Regenerate XML, API pages and HTML without reusing build caches." ) + parser.add_argument( + "--force-pinned", action="store_true", help="Select the locked documentation tools instead of compatible installed tools." + ) + parser.add_argument("--cache-dir", type=Path, help="Shared documentation tool cache directory.") parser.add_argument("--doxygen", help="Use this Doxygen executable explicitly.") arguments = parser.parse_args() - if arguments.doxygen is not None and os.environ.get("CHERYL_DOCS_FORCE_PINNED") == "1": + force_pinned = arguments.force_pinned or os.environ.get("CHERYL_DOCS_FORCE_PINNED") == "1" + if arguments.doxygen is not None and force_pinned: parser.error("An explicit Doxygen override cannot be combined with forced pinned tools.") - if os.environ.get("CHERYL_DOCS_TOOLS_ACTIVE") != "1": + select_tools = os.environ.get("CHERYL_DOCS_TOOLS_ACTIVE") != "1" + if arguments.force_pinned and os.environ.get("CHERYL_DOCS_FORCE_PINNED") != "1": + select_tools = True + if arguments.cache_dir is not None: + selected_cache = os.environ.get("CHERYL_DOCS_CACHE_DIR") + if selected_cache is None or arguments.cache_dir.expanduser().resolve() != Path(selected_cache).expanduser().resolve(): + select_tools = True + if select_tools: overrides = ["--doxygen", arguments.doxygen] if arguments.doxygen is not None else [] + if force_pinned: + overrides.append("--force-pinned") + if arguments.cache_dir is not None: + overrides.extend(["--cache-dir", str(arguments.cache_dir)]) return subprocess.call( [ sys.executable, diff --git a/generate-docs.sh b/generate-docs.sh new file mode 100755 index 00000000..b85cbab8 --- /dev/null +++ b/generate-docs.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_directory="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" + +if [[ -n "${CHERYL_DOCS_PYTHON:-}" ]]; then + docs_python="$CHERYL_DOCS_PYTHON" +elif command -v python3 >/dev/null 2>&1; then + docs_python=python3 +elif command -v python >/dev/null 2>&1; then + docs_python=python +else + printf '%s\n' 'Documentation generation requires Python 3.11 or newer.' >&2 + exit 127 +fi + +exec "$docs_python" "$script_directory/docs/generation/build.py" "$@" From e8287fec51149250d969f7bcdd013058df4e824b Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 15:04:49 -0700 Subject: [PATCH 13/98] Clarifies getting started terminology with concrete API and build names Connects factory functions, startup/parser objects, contexts, resource providers, draw packets and text stages to their exact types, calls and local variables. Clarifies CMake targets, SDK prefixes and binary compatibility while preserving maintained examples and thread/lifetime contracts. --- docs/getting-started/first-game.md | 251 +++++++++++++++++--------- docs/getting-started/project-setup.md | 151 +++++++++------- 2 files changed, 257 insertions(+), 145 deletions(-) diff --git a/docs/getting-started/first-game.md b/docs/getting-started/first-game.md index aea127cb..819c9229 100644 --- a/docs/getting-started/first-game.md +++ b/docs/getting-started/first-game.md @@ -58,10 +58,16 @@ not establish those graphics and input observations. ### Startup options, including your own -{cpp:func}`CE::Application::GLFWOpenGL::make_startup` selects the native GLFW -window/input adapter and OpenGL rendering/resource adapters. Its configuration -provides the application's defaults. Register custom command-line options before -calling `initialize()`: +{cpp:func}`CE::Application::GLFWOpenGL::make_startup` is a **factory function**: +it constructs and returns a {cpp:class}`CE::Application::Startup` object. The +example stores that object in the variable `startup`. It holds the command-line +parser and the configuration used to create the engine context. + +This function selects GLFW for the window/input implementation and OpenGL for +drawing and graphics resource creation. These implementations are the +**backends**; their objects implement the engine's window, input, rendering, and +resource interfaces. Register your game's command-line options on `startup` +before calling `startup.initialize()`: ```{literalinclude} ../../projects/apps/first-game/src/main.cpp :language: cpp @@ -71,62 +77,93 @@ calling `initialize()`: :dedent: 8 ``` -The factory's first argument is the command-line description. The configuration -sets a 640 × 360 window and its title. Cheryl also registers engine/backend -options: use `--help` to see those available in this build. For example, -`--window-width` overrides the initial width, `--worker-count` controls CPU worker -capacity, and `--concurrent` selects the separate simulation thread. +In `make_startup("Cheryl first game", {...})`, the first argument is the text +shown in command-line help. The second argument, `{.width = 640, ...}`, is a +`CE::Application::GLFWOpenGL::Options` configuration value: it supplies defaults +such as window size and title. + +The calls in `main()` act on different objects: -`add_application_options()` lets your game use the same CLI11 parser. Here, -`--assets` writes into `asset_root`, which stays alive through parsing. CMake +| Call in the example | Object and role | +| --- | --- | +| `startup.add_application_options(...)` | A `Startup` member function. It immediately calls the supplied lambda to register your game's options. | +| `options.add_option("--assets", asset_root, ...)` | A CLI11 member function. The lambda's `CLI::App& options` refers to the **Application option group** within `startup`'s parser. This call registers `--assets` and binds its value to `asset_root`. | +| `startup.initialize(argc, argv)` | Parses the command line, creates the engine context, and returns the `StartupResult` stored in `result`. | +| `result.make_runtime(game)` | Creates the `GameRuntime` stored in `runtime`, using the parsed settings and your `game` object. | +| `runtime.run()` | Runs the engine loop and calls your game's lifecycle functions. | + +The callback's `options` parameter is a parser interface; the `Options` value +passed to `make_startup()` is configuration data. Cheryl also registers its own +engine/backend options: use `--help` to see those available in this build. +For example, `--window-width` overrides the initial width, `--worker-count` +controls CPU worker capacity, and `--concurrent` selects the separate simulation +thread. + +When `startup.initialize()` parses `--assets`, it writes into the `asset_root` +string captured by the lambda. Keep that string alive through parsing. CMake supplies its default as an absolute path, so launching from another directory still finds the same assets. An explicit relative override resolves from the process's working directory. Add other game-specific options in that callback -and keep their destination variables alive until `initialize()` returns. +and keep their destination variables alive until `startup.initialize()` returns. ### Configure the context and run the game -The factory's {cpp:struct}`CE::Application::GLFWOpenGL::Options` configuration +The second argument to `make_startup()`, the +{cpp:struct}`CE::Application::GLFWOpenGL::Options` value, selects the initial window dimensions, title, window mode, and swap interval. Its `execution` member configures the context's CPU execution service, including -worker count and an optional shared pool. The factory also accepts -{cpp:struct}`CE::Application::RuntimeOptions` for loop defaults such as execution -mode and simulation timing. Parsed command-line options override these defaults. +worker count and an optional shared pool of threads for background tasks. +`make_startup()` also accepts a {cpp:struct}`CE::Application::RuntimeOptions` +value as an optional third argument, omitted above. It supplies loop defaults +such as execution mode and simulation timing. Parsed command-line options +override these defaults. See [application startup](../development/consuming-engine.md#command-line-startup) for the complete option and validation contract. -`initialize(argc, argv)` parses and validates options, then creates the owned -{cpp:class}`CE::Runtime::Context` and native window with the chosen adapters and -services. Help and argument errors return without a context: check -`should_start()` before using it and return the supplied exit code. Backend -creation failures can throw; the complete example wraps `main()`'s body in an -exception handler. - -Construct your game with that context, then use `result.make_runtime(game)` to -apply the parsed loop settings together. `runtime.run()` starts the rendering -and input adapters, calls the game hooks, runs until stopped, and shuts those -adapters down. Resource upload becomes available inside `Game::init()`. - -Declare the startup result, game, and runtime in that order. The game borrows the -context; the runtime borrows both. Reverse destruction order keeps each owner -alive for its borrowers. The context provides services; the runtime owns the -loop that calls your game. +`result` is a {cpp:struct}`CE::Application::StartupResult`. +On successful initialization, its `result.context` member owns a +{cpp:class}`CE::Runtime::Context`, the object grouping the chosen engine services +and backend implementations. The GLFW window exists at this point. Help and +argument errors return without a context: check `result.should_start()` before +using it and return `result.exit_code`. Backend creation failures can throw; the +complete example wraps `main()`'s body in an exception handler. + +Construct the `FirstGame::Game` instance named `game` with `*result.context`. +`result.make_runtime(game)` returns a {cpp:class}`CE::Runtime::GameRuntime`, +stored in `runtime`, with the parsed loop settings. `runtime.run()` starts the +rendering and input implementations, calls the game's lifecycle functions, runs +until stopped, and shuts rendering and input down. Uploading graphics resources +becomes available inside `Game::init()`. + +Declare `result`, `game`, and `runtime` in that order. Here, **owns** means being +responsible for an object's lifetime; **borrows** means using an object that +someone else must keep alive. The game borrows the context, and the runtime +borrows both. Reverse destruction order keeps those dependencies alive. The +context provides services; the runtime controls the loop that calls your game. ## Engine architecture: the game developer's view ### Game state and lifecycle -Implement {cpp:struct}`CE::Game::AbstractGame`. Keep the camera, actors, selected -resources, controls, and other game state in your implementation: +{cpp:struct}`CE::Game::AbstractGame` is the base interface for a game. Derive your +own class from it and implement its virtual member functions, called **hooks** +because the runtime calls them at defined points in the lifecycle. The example's +implementation is `FirstGame::Game`, the class instantiated as `game` in `main()`: ```{literalinclude} ../../projects/apps/first-game/src/first-game.h :language: cpp -:caption: Game — initial state is prepared in init(), then managed by simulation +:caption: FirstGame::Game — initial state is prepared in init(), then managed by simulation :start-after: // [game] :end-before: // [/game] :dedent: 4 ``` +Its `engine_` member is a `CE::Runtime::Context&` referring to `*result.context`. +Thus calls such as `engine_.input()` use the context created in `main()`. +`camera_` stores the view, `actor_` stores the sprite actor, `score_` stores the +uploaded text label, and `images_`/`text_` retain their drawing materials. The +sprite and text sections below introduce those resource types. + The **platform thread** is the thread calling `runtime.run()`. It polls native events and owns the graphics context. The **simulation owner** runs your game updates and frame preparation. By default, it is that same thread. With @@ -139,11 +176,12 @@ still run in order on that owner. | `update(tick)` | Simulation owner | Change game state using copied input, dimensions, and simulation time. Request a stop when appropriate. | | `prepare_render_frame(frame) const` | Same simulation owner | Turn current state into a complete frame of draw values and retained resources. Keep gameplay progression in `update()`; frame preparation can be skipped when no frame slot is available. | | `quiesce()` | Platform thread, after simulation stops and joins | Detach external listeners and stop producers that could submit more work. The default hook is enough for this example. | -| `deinit()` | Platform/graphics thread, after accepted work settles and frames are recycled | Remove bindings and release game-owned resources while input and graphics still exist. | +| `deinit()` | Platform/graphics thread, after submitted background work finishes and frames are released for reuse | Remove bindings and release game-owned resources while input and graphics still exist. | Initialization prepares the state on the platform thread before simulation begins. During the loop, simulation owns changes to that state. Rendering reads a -**published frame**: copied camera/draw values and retained resource handles. +**published frame**: a complete set of drawing instructions for one window +image, containing copied camera/draw values and retained resources. After your frame-preparation hook returns, the runtime publishes the completed frame. @@ -151,19 +189,27 @@ That boundary lets native events and graphics stay on their required thread while simulation can run independently. Reading a live actor position from the renderer while simulation changes it would be a data race. Instead, the renderer can draw an earlier frame's copied position while the actor moves again. -`shared_ptr` keeps a resource alive; it does not make concurrent mutation safe. +The sprite/material **handles** in this example are `shared_ptr` values: holding +one keeps the referenced resource alive. That does not make concurrent mutation +safe. Publish a new resource version when its contents change. ### Interfaces you use +`engine_` gives the game access to these interfaces. An **interface** describes +the operations an implementation provides; for example, `engine_.resources()` +returns a `CE::Rendering::Resources::Provider&`. That **resource provider** creates +the backend's graphics resources from prepared data. Later snippets store this +reference in the local variable `resources`. + | Interface | When and why your game uses it | | --- | --- | -| `Context::input()` | Bind actions in `init()` and unbind them in `deinit()`. Read gameplay input from `tick.input` during updates. | -| `Context::window()` | Read native window state on the platform thread. During updates, use `tick.framebuffer_size`, the copied dimensions for simulation. | -| `Context::resources()` | Upload prepared graphics resources on the graphics owner, initially in `init()`. | -| `TickContext` | Read the simulation delta/input/dimensions and call `request_stop()`. Its borrowed data is valid for that update. | -| `FrameWriter` and its passes | Build a CPU-side frame during `prepare_render_frame()`. Do not retain the writer or passes after the hook returns. | -| `Context::make_worker_group()` and `platform_dispatcher()` | Later, prepare expensive resources on tracked CPU workers and hand owned upload requests to the platform thread. | +| `engine_.input()` | Bind actions in `init()` and unbind them in `deinit()`. Read gameplay input from `tick.input` during updates. | +| `engine_.window()` | Read native window state on the platform thread. During updates, use `tick.framebuffer_size`, the copied dimensions for simulation. | +| `engine_.resources()` | Obtain the resource provider for uploads on the graphics owner, initially in `init()`. | +| `tick`, the `TickContext` parameter of `update()` | Read the simulation delta/input/dimensions and call `tick.request_stop()`. Its borrowed data is valid for that update. | +| `frame`, the `FrameWriter` parameter of `prepare_render_frame()` | Build drawing instructions on the CPU. Do not retain the writer or passes after the hook returns. | +| `engine_.make_worker_group()` and `engine_.platform_dispatcher()` | Later, prepare expensive resources on background CPU threads and queue upload requests for the platform thread. | Calling a context accessor does not move execution to another thread. In particular, graphics uploads in concurrent `update()` would run on the wrong @@ -185,11 +231,12 @@ which generation to submit. Keep those choices with simulation. | Font and rendered text | Load a font collection, lay out the message, prepare CPU images/triangles, then upload a text generation. | Retain the generation and resolve it into each frame that displays the label. | Replace or reset the generation; local font/layout variables need not remain in scope. | | Sound effect or music | With the optional audio module, decode a clip, then play it through an application-owned audio system; or open a stream there. | Playback voices run on the audio system's own timeline; they do not pass through rendering frames. | Explicitly stop a voice when playback should end; dropping a voice handle alone does not stop it. Close the audio system after its producers stop. | -Releasing a game handle ends that owner's use. Caches and published frames may -still retain the resource, so it does not promise immediate native destruction. -Do not clear a shared cache just because an actor disappears. Keep the originating -backend alive until its resources have been released and retired; runtime -shutdown provides that ordering for this game's graphics. See +Releasing a game handle ends that owner's use. The shared asset caches and +published frames may still retain the resource, so this does not immediately +delete its GPU textures or buffers. Do not clear a shared cache just because an +actor disappears. Keep the backend that created the resources alive until their +references are released and backend cleanup finishes; runtime shutdown provides +that ordering for this game's graphics. See [resource ownership](../resources/resource-residency.md) and [audio playback](../runtime/audio.md) for their respective lifetime contracts. @@ -199,8 +246,12 @@ These are shared foundations for the sprite and text examples below. ### Bind controls during initialization -Native key codes become semantic game actions at the input boundary. Retain the -returned IDs so cleanup can remove this game's bindings: +An **action** is a game-defined input name represented by a +`CE::Input::Actions::Id`; here, `left_`, `right_`, `next_cell_`, and `quit_` are +those IDs. A **binding** maps a device input to an action. In the following code, +`bindings` is the binding interface returned by `engine_.input().bindings()`. +Its `bind_button()` calls map Gainput key codes to the game's actions and return +binding IDs, stored in the `bindings_` member for cleanup: ```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp :language: cpp @@ -216,9 +267,11 @@ actions, events, and focus handling. ### Understand what a draw needs -A sprite or uploaded label supplies geometry and images. A **material** supplies -the drawing recipe: shader program, geometry topology, sampling, and parameter -bindings. The example loads both recipes from its +A sprite or uploaded label supplies geometry (vertices forming shapes) and image +pixels. A **material**, represented by `CE::Rendering::Resources::Material`, +supplies the drawing recipe: a shader program to run on the GPU, how vertices +form triangles (topology), how image pixels are sampled, and named parameters. +The members `images_` and `text_` retain the two materials loaded from the [material manifest](../../projects/apps/first-game/assets/graphics/shaders/materials.json): | Material | Geometry and sampling | @@ -237,9 +290,13 @@ it from the actual framebuffer dimensions before placing the actor: :dedent: 8 ``` -Updates will keep the camera's dimensions current using the tick's copied size. -During frame preparation, open a pass with that camera and describe its common -image parameter: +Updates will keep `camera_`'s dimensions current using the tick's copied size. +In `prepare_render_frame(CE::Rendering::FrameWriter& frame)`, `frame` is the +runtime's borrowed **frame writer**, used to assemble this frame's instructions. +`frame.begin_pass(...)` returns the local `pass`, a +`CE::Rendering::PassWriter`. A **pass** groups draws that share camera and pass +settings. The local `context` is a `CE::Rendering::Submission::Context2D` value +containing those settings for the draw-resolution functions below: ```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp :language: cpp @@ -249,15 +306,22 @@ image parameter: :dedent: 8 ``` -`begin_pass()` copies the {cpp:class}`CE::Rendering::Camera2D` projection and view -matrices. -{cpp:struct}`CE::Rendering::Submission::Context2D` describes the pass values and -selects the public `image` parameter at texture unit zero. Each draw then adds a -{cpp:struct}`CE::Rendering::DrawStyle2D` with its material, model transform, scale, -and alpha. The following two walkthroughs add to this same pass. - -The material maps public parameter names to native shader uniforms: the code -uses `image`, while this OpenGL recipe calls that sampler `mytexture`. The +`frame.begin_pass()` copies `camera_`'s projection and view matrices, which map +game coordinates to the window. The draw-settings value `context` is separate +from the engine's `CE::Runtime::Context` referenced by `engine_`. Its +`ImageParameter2D{"image", 0}` tells resolution where to place each asset's image: +the material parameter named `image`, using texture binding slot zero. + +For each draw, the local `image_style` or `text_style` is a +{cpp:struct}`CE::Rendering::DrawStyle2D` value. It selects a material and supplies +the model transform (position/orientation), scale, and alpha (opacity). Resolution +combines an asset, a style, and the pass settings into a +`CE::Rendering::DrawPacket2D`: an owned drawing instruction. The following two +walkthroughs add those packets to this same `pass` with `pass.add(...)`. + +The material maps public parameter names to named shader inputs, called +**uniforms** in OpenGL: the code uses `image`, while this shader calls that image +input `mytexture`. The [vertex shader](../../projects/apps/first-game/assets/graphics/shaders/first-game.vert) combines the pass's projection/view with each draw's model transform. See [pipelines and materials](../rendering/pipelines-and-materials.md) when writing @@ -283,7 +347,9 @@ assets/ └── textures/actor.png ``` -The index chooses which definition files to load: +`graphics-manifests.json` is an **index**: it lists the JSON definition files +the loader should read. Each listed **manifest** describes named assets or +drawing materials: ```{literalinclude} ../../projects/apps/first-game/assets/graphics/graphics-manifests.json :language: json @@ -313,7 +379,11 @@ as game-owned values: :dedent: 4 ``` -Load the root, check the required named resources, and construct the actor: +The local `loader` is a `CE::Assets::Loading::Loader` that reads this asset root. +`resources` refers to the resource provider from `engine_.resources()`. +`Sprites::Manager::get()` and `Shaders::Manager::get()` access the shared caches +used to look up loaded sprites and materials. Load the root, check the required +resources, and construct `actor_`: ```{literalinclude} ../../projects/apps/first-game/src/first-game.cpp :language: cpp @@ -331,9 +401,12 @@ assets, so loading does not also select unrelated or optional artwork. An upload failure can leave already completed cache entries; the [asset-loading guide](../assets/asset-loading.md) explains validation and retry. -`get_asset("first-game", "actor")` looks up `first-game:actor` and returns a -retained handle or null. `get_assets("first-game")` returns that namespace keyed -by local name when a game system needs several sprites. Resolve names once and +The sprite manifest's `"namespace": "first-game"` groups asset names; its +`"sprites"` object contains the local name `"actor"`. Together they identify +`first-game:actor`. This asset namespace is a naming group within the loaded +resources. `get_asset("first-game", "actor")` returns its shared sprite handle +or null. `get_assets("first-game")` returns that group keyed by local name when a +game system needs several sprites. Resolve names once and pass handles into the owning actors/managers; collection iteration order is not gameplay identity. The local loader can go out of scope after initialization. The actor's retained handle remains valid within its backend's lifetime. @@ -353,8 +426,9 @@ without reading the live native window. Escape requests normal runtime shutdown. Space advances the actor's selected image cell; the shared sprite stays unchanged. This asset has no timed animation clip. For authored clips, keep a separate -{cpp:class}`CE::Assets::Sprites::Animation` cursor per actor and advance it with -simulation time. See [asset values and playback](../assets/asset-values-and-playback.md). +{cpp:class}`CE::Assets::Sprites::Animation` object per actor to hold its playback +position, and advance it with simulation time. See +[asset values and playback](../assets/asset-values-and-playback.md). ### Prepare a frame: publish the actor's current appearance @@ -391,15 +465,16 @@ The string literal in this example follows that same path. ### Init: turn the message into a drawable label -Each stage answers a different question: +`selection` is a `CE::Text::FontSelection` value describing which fonts to look +for and where to search. The calls below produce these successive values: | Stage | What it produces and why | | --- | --- | -| `FontCollection::load(selection)` | Font data describing letter and symbol shapes, with fallback fonts for missing characters. | -| {cpp:func}`CE::Text::layout_text` | **Which glyphs to draw and where to put them**, at the requested font size. A glyph is a font's drawing for a letter, symbol, or group of characters. Choosing glyphs and adjusting their spacing is called **shaping**; letters can join or share a glyph. Layout also handles direction and line breaks. It produces no pixels. | -| {cpp:func}`CE::Text::Rendering::prepare` | CPU images of the needed glyphs packed into sheets called **atlas pages**, plus triangles selecting each glyph's image. This turns font outlines into drawable pixels. | -| {cpp:func}`CE::Text::Rendering::upload` | A {cpp:class}`CE::Text::Rendering::Generation` holding the layout and graphics resources. In this application, images and triangles become OpenGL textures and buffers. | -| `resolve_text()` and `pass.add()` below | A frame's drawing instructions combining that uploaded label with a material and position. The runtime's renderer executes them to display the text. | +| `FontCollection::load(selection)` | `fonts`, a `CE::Text::FontCollection` containing font data describing letter and symbol shapes, with fallback fonts for missing characters. | +| {cpp:func}`CE::Text::layout_text` | `layout`, a `CE::Text::ShapedText` describing **which glyphs to draw and where to put them**, at the requested font size. A glyph is a font's drawing for a letter, symbol, or group of characters. Choosing glyphs and adjusting their spacing is called **shaping**; letters can join or share a glyph. Layout also handles direction and line breaks. It produces no pixels. | +| {cpp:func}`CE::Text::Rendering::prepare` | A `CE::Text::Rendering::Prepared` value: CPU images of the needed glyphs packed into sheets called **atlas pages**, plus triangles selecting each glyph's image. This turns font outlines into drawable pixels. | +| {cpp:func}`CE::Text::Rendering::upload` | A {cpp:class}`CE::Text::Rendering::Generation`, stored in the game's `score_` member, holding the layout and graphics resources. In this application, images and triangles become OpenGL textures and buffers. | +| `resolve_text()` and `pass.add()` below | `DrawPacket2D` drawing instructions combining that uploaded label with a material and position, added to `pass`. The runtime's renderer executes the published frame to display the text. | The initialization code follows those stages. `resources` is the provider reference obtained in the asset-loading excerpt: @@ -468,8 +543,8 @@ to draw and present. ### Deinit: release the uploaded label `score_.reset()` and `text_.reset()` release this game's generation and text -material on the platform thread during the cleanup hook below. Backend resource -retirement follows retained ownership. See the +material on the platform thread during the cleanup hook below. Frames and caches +can still retain their resources, as described in the ownership section. See the [Unicode text guide](../assets/text-layout.md) for wrapping and more advanced replacement behavior. @@ -499,15 +574,23 @@ shutdown and failure ordering. ## Load or replace resources during play -The same resource path can span threads when preparation is expensive: +The same resource path can span threads when preparation is expensive. +`engine_.make_worker_group()` returns a `CE::Workers::Group`, which tracks jobs +submitted to background CPU threads. `engine_.platform_dispatcher()` returns a +`CE::Runtime::PlatformDispatcher&`, the queue for work that must run on the +platform thread. Its submitted callback receives a `CE::Runtime::Context&` to +use on that thread; a worker-group callback takes no engine argument. Either +object's `submit(...)` returns a `std::future`: a way to retrieve the eventual +result of that job. 1. **Simulation:** request a load through an Engine worker group and keep the current resource while the request is pending. 2. **CPU worker:** prepare owned data, such as `Loader::prepare()` results or a text layout and atlas. Keep the loader and request inputs alive through their work. -3. **Platform/graphics thread:** receive the owned prepared value through the - platform dispatcher, upload it, and return retained resources through a future. +3. **Platform/graphics thread:** receive the owned prepared value through + `engine_.platform_dispatcher().submit(...)`, upload it, and return retained + resources through that request's future. 4. **Simulation, during `update()`:** check readiness and adopt the completed result. Later frames select the new version; earlier frames retain the old one. diff --git a/docs/getting-started/project-setup.md b/docs/getting-started/project-setup.md index 8c9229c6..27f70d67 100644 --- a/docs/getting-started/project-setup.md +++ b/docs/getting-started/project-setup.md @@ -1,15 +1,17 @@ # Project setup -Choose how your game obtains Cheryl before writing its entry point. During +Choose how your game obtains Cheryl before writing `main()`. During development, adding a source checkout to your game's CMake project is sufficient: -building the game also builds the libraries it uses. An installed development SDK -is an alternative when you want to consume already compiled libraries. Both paths -provide the same CMake target names and public `cheryl/` headers. +building the game also builds the libraries it uses. A development **SDK** is a +directory containing compiled libraries, public headers and CMake package files; +use it when you want to link already compiled Cheryl libraries. Both paths +provide the same public CMake names, such as `Cheryl::Cheryl`, for +`target_link_libraries()`, and the same `cheryl/` headers. After setup, continue with [Your first game](first-game.md). It introduces startup, the game hooks and resource ownership before putting a sprite and text on screen. -The [consumption reference](../development/consuming-engine.md) covers other target -compositions and the full SDK contract. +The [consumption reference](../development/consuming-engine.md) covers other +library selections and the full SDK requirements. ## Dependencies @@ -17,19 +19,20 @@ Use CMake 3.28 or newer, Ninja, and a C++23 compiler and standard library with `std::format` support. Install the [system dependencies](../development/building.md#system-dependencies) required by your selected modules. The first game uses FreeType, HarfBuzz and ICU for text, -plus Native GLFW, native input and OpenGL for its window, controls and rendering. +plus `Cheryl::NativeGLFW` for its window/input and `Cheryl::OpenGL` for rendering. It needs a desktop graphics driver supporting an OpenGL 3.3 core context. For source builds, initialize the checkout's recorded recursive submodules. Python and Jinja2 are needed when GLAD generates OpenGL bindings. The first game supplies -its own sprite, shaders and manifests; the embedded fallback font comes from the -assets submodule. No optional artwork download is needed. +its own sprite image, shaders and `assets/graphics/graphics-manifests.json`, an +index selecting its graphics definitions. The embedded fallback font comes from +the engine checkout's `assets/` submodule. No optional artwork download is needed. For an SDK, install its [consumer prerequisites](../development/consuming-engine.md#prebuilt-development-sdks) instead of rebuilding Cheryl. Some native development/runtime libraries remain -external on Linux; Windows SDKs include a dependency prefix whose DLLs must be -available at runtime. +external on Linux; Windows SDKs bundle native libraries under `dependencies/`, +whose `.dll` files must be available when your game runs. ## Build your game with a source checkout @@ -63,10 +66,10 @@ your chosen parent directory. This block preserves your shell's starting directo Copy the maintained [first game's](../../projects/apps/first-game/) `src/` and `assets/` directories into `my-game/` to use its code unchanged. Create -`my-game/CMakeLists.txt` as follows. The options select the first game's -native graphics/input composition and omit unrelated applications and integrations. -The block scopes those settings to Cheryl; it does not force changes into your -project's CMake cache. +`my-game/CMakeLists.txt` as follows. The `CHERYL_BUILD_*` options select the first +game's Native GLFW/OpenGL libraries and omit unrelated applications and integrations. +`block(SCOPE_FOR VARIABLES)` limits those settings to the enclosed Cheryl setup; +it does not replace saved command-line/IDE options in `CMakeCache.txt`. ```cmake cmake_minimum_required(VERSION 3.28) @@ -101,21 +104,31 @@ target_compile_definitions(my_game PRIVATE CHERYL_FIRST_GAME_ASSETS="${CMAKE_CURRENT_SOURCE_DIR}/assets") ``` -The scoped `set()` values fix this template's component selection; edit them +`add_subdirectory()` reads the engine checkout's CMake project and places its +build files under the game's `cheryl/` build subdirectory. `add_executable()` +creates the executable target named `my_game`; `target_link_libraries()` attaches +the selected Cheryl libraries and their compiler settings to it. + +The scoped `set()` values fix this template's module selection; edit them when adding a module. A same-name `-D` cache option does not override those local values. `CHERYL_FIRST_GAME_ASSETS` supplies the example's default asset-root string, so launching from an IDE or another directory still finds your game's files. As you replace the example with your own game, keep that explicit connection between -the CMake target and the asset path used by startup. +the executable target and the asset path read by `main()`. + +`Cheryl::Cheryl` is an **interface target**: it links Engine and the selected +module/support libraries and supplies their include directories and preprocessor +definitions. It produces no combined library of its own. Your sources can use +`#include `: this aggregate header includes Engine and the public +module/support headers selected by those definitions. For example, linking +`Cheryl::OpenGL` supplies `CHERYL_HAS_OPENGL=1`, which enables +`` inside the aggregate header. -`Cheryl::Cheryl` provides Engine and the selected modules/support, including their -include directories, feature definitions and dependencies. Your sources can use -`#include ` without collecting individual include paths. Changing -a `CHERYL_BUILD_*` option makes a component available; linking its target is what -provides that component to the application. The facade links the configured set. -Reload CMake in the IDE after changing selections. +A module option such as `CHERYL_BUILD_OPENGL` makes `Cheryl::OpenGL` available; +linking its target supplies that library to your game. `Cheryl::Cheryl` links the +configured set. Reload CMake in the IDE after changing selections. Configure and build from the game directory. Replace `/path/to/projects/my-game` with its location: @@ -131,6 +144,11 @@ Configure and build from the game directory. Replace ) ``` +`cmake -S . -B build/release` configures the build. `CMAKE_BUILD_TYPE=Release` +selects the optimization/debug/logging configuration; module options such as +`CHERYL_BUILD_OPENGL` select libraries. `cmake --build ... --target my_game` +then compiles that executable and the libraries it links. + The policy setting accommodates pinned legacy dependencies on CMake 4 hosts. The GLFW setting chooses X11 on Linux; the Gainput setting avoids the optional HID backend and its dependency download. Use the @@ -141,37 +159,46 @@ follow [First game: setup](first-game.md#setup). ### Choose smaller target sets when needed -The facade is convenient for a game using the configured assembly. Granular -targets let a game select fewer dependencies: +Link `Cheryl::Cheryl` for all selected libraries, or choose individual public +targets for fewer dependencies: | Link target | What the game obtains | | --- | --- | -| `Cheryl::Cheryl` | Engine and the configured module/support assembly, including the Linux Debug terminal when selected in a source build. | -| `Cheryl::OpenGL::Startup` | Common command-line startup plus Native GLFW/OpenGL and their Engine requirements; enough for the first game's startup path. | -| `Cheryl::Engine` | Neutral Engine contracts and implementation, without selecting a native backend. | +| `Cheryl::Cheryl` | Engine and the configured module/support libraries, including the Linux Debug terminal when selected in a source build. | +| `Cheryl::OpenGL::Startup` | The `CE::Application::GLFWOpenGL::make_startup(...)` function used by `main()`, plus common startup, Native GLFW/OpenGL and their Engine requirements. | +| `Cheryl::Engine` | Neutral Engine interfaces and implementation, without selecting a window or graphics implementation. | | `Cheryl::Audio::Miniaudio` | Selected audio integration; add it when your game needs sound. | | `Cheryl::UI::TGUI` or `Cheryl::UI::RmlUi` | The selected UI integration. | -For example, replace the facade link with +For example, replace the `Cheryl::Cheryl` link with `target_link_libraries(my_game PRIVATE Cheryl::OpenGL::Startup)` for the native -startup composition alone. The [target reference](../../README.md#targets) lists -additional support targets. Header umbrellas expose only components supplied by -the linked targets, so a narrower link can make a feature's conditional include -inactive even when its build option is enabled. +startup libraries alone. In source builds, names such as `Cheryl::Engine` are +**aliases** for concrete build targets: link `Cheryl::Engine`, but use +`cmake --build ... --target cengine` to build its library directly. SDKs expose +the same public names as imported targets referring to existing libraries. The +[target reference](../../README.md#targets) maps the other public and build names. + +`` includes only the modules supplied by linked targets, so a +narrower link can make a feature's conditional include inactive even when its +build option is enabled. ## Use an installed SDK instead -Choose an SDK containing Native GLFW, OpenGL, native input and Startup for the -first game. Its package must match your OS, architecture, compiler/standard-library -ABI and configuration. With an extracted SDK, replace the source-checkout block -in the game CMake file with: +Choose an SDK providing `Cheryl::OpenGL::Startup` and Native GLFW with native input +for the first game. Its package must match your OS, CPU architecture, +compiler/standard-library **ABI** (the binary conventions needed to link C++ code) +and build configuration, such as Release. With an extracted SDK, replace the +source-checkout `block(...)` through `endblock()` in the game CMake file with: ```cmake find_package(Cheryl 4 CONFIG REQUIRED) ``` -Keep `add_executable`, `target_link_libraries` and your asset-root definition. -Configure with the extracted SDK prefix; replace both paths below: +`find_package()` loads the SDK's `CherylConfig.cmake`, which provides its public +targets and dependency settings. Keep `add_executable`, `target_link_libraries` +and your asset-root definition. The **SDK prefix** is the extracted root directory +containing `include/`, libraries and CMake package files. Set `CMAKE_PREFIX_PATH` +to that directory so `find_package()` can locate it; replace both paths below: ```sh ( @@ -192,9 +219,10 @@ in SDKs. ## Build the maintained example independently -The first game's own CMake entry point also works outside the engine's top-level -build, without copying its files or writing another CMake project. From anywhere -inside the checkout, configure against engine source: +The first game's own `projects/apps/first-game/CMakeLists.txt` also works outside +the engine's top-level build, without copying files or writing another CMake +project. `CHERYL_FIRST_GAME_ENGINE_SOURCE` selects an engine checkout for that +project. From anywhere inside the checkout, configure against engine source: ```sh ( @@ -209,8 +237,9 @@ inside the checkout, configure against engine source: ) ``` -Alternatively, leave the source option empty and select a compatible SDK -containing Native GLFW, native input, OpenGL, and Startup: +Alternatively, leave `CHERYL_FIRST_GAME_ENGINE_SOURCE` empty to use +`find_package(Cheryl)` and select a compatible SDK containing +`Cheryl::OpenGL::Startup` and Native GLFW with native input: ```sh ( @@ -230,35 +259,35 @@ have an `.exe` suffix. ## Produce engine artifacts -The SDK is optional for source consumers. When producing engine artifacts for -others, choose the corresponding operation rather than building every target: +The SDK is optional for source consumers. To produce a file or directory for +others, choose its build, install or packaging operation: | Need | Operation | Result | | --- | --- | --- | | Run the maintained tutorial | Select `CHERYL_BUILD_FIRST_GAME=ON`; build `first-game`. | The tutorial executable and its linked libraries. | | Build a library directly | Build `cengine`, `module_opengl` or another concrete library target. | The selected library and dependencies in the build tree. | -| Build selected SDK libraries | Select `CHERYL_BUILD_SDK=ON`; build `cheryl-sdk`. | Compiled installable libraries; headers have not yet been collected. | -| Collect libraries and headers | After that build, install component `CherylSDK` to a prefix. | One SDK tree with headers, libraries, CMake imports and licenses. | -| Create a distributable SDK archive | Run `.github/ci/package.py` after the SDK build. | Archive, manifest and checksums; Windows also bundles its native dependencies. | -| Build the website and API reference | Build `docs` in the documentation-only project. | Generated HTML, without compiling Engine. | +| Build selected SDK libraries | Select `CHERYL_BUILD_SDK=ON`; build custom target `cheryl-sdk`, which builds the selected libraries together. | Compiled installable libraries; headers have not yet been collected. | +| Collect libraries and headers | After that build, use `cmake --install` with `--component CherylSDK` and a destination `--prefix`. | One SDK directory with headers, libraries, CMake package files and licenses. | +| Create a distributable SDK archive | Run `.github/ci/package.py` after the SDK build. | Archive, `SDK-MANIFEST.json` describing its build, and `SHA256SUMS` for archive verification; Windows also bundles its native dependencies. | +| Build the website and API reference | Run `./generate-docs.sh`, or build `docs` in the documentation-only project. | Generated HTML, without compiling Engine. | Use the [artifact commands](../development/building.md#choose-the-artifact) for -exact configuration, build, install and packaging blocks. `Cheryl::Cheryl` is an -interface joining targets; it does not produce one combined binary. +exact configuration, build, install and packaging blocks. ### Keep headers and binaries matched -A build's selections determine which module libraries, support libraries and -dependencies exist. Configuration also affects compiled logging and Debug -instrumentation. Keep separate build/prefix directories for different configurations -and module selections, and compile game translation units against the headers and +A build's module selections determine which libraries and dependencies exist. +`CMAKE_BUILD_TYPE` also affects compiled logging and Debug instrumentation. Keep +separate build directories and installed SDK prefixes for different configurations +and module selections. Compile each game `.cpp` file against the headers and target settings that belong to those libraries. For a packaged SDK, check its `SDK-MANIFEST.json` for the producer's source/submodule revisions, features, toolchain and configuration. Matching filenames alone do not establish compatibility. -Engine, Startup and the current compiled module implementations explicitly build -as static libraries. Setting `BUILD_SHARED_LIBS=ON` does not convert these targets -to shared libraries. Some third-party/native dependencies have their own linkage -choices. A shared-library distribution and its binary compatibility policy require -a separate design decision; use the current source-target or matched-SDK contract +`Cheryl::Engine`, `Cheryl::Startup` and the current compiled module implementations +build as static libraries, whose code is linked into the executable. Setting +`BUILD_SHARED_LIBS=ON` does not convert these targets to shared libraries loaded +at runtime. Some third-party/native dependencies have their own linkage choices. +A shared-library distribution and its binary compatibility policy require a +separate design decision; use the current source-build or matched-SDK approach for this walkthrough. From 67246373436cad6d4ab7062fbbdb495a3c50a72e Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 15:06:56 -0700 Subject: [PATCH 14/98] Fixes incremental documentation invalidation and enum references Canonicalizes source exclusions so output directories appearing after the initial build do not invalidate every Sphinx doctree. Resolves public enumerators through their real Doxygen owner pages and anchors when cached C++ symbol lookup loses an unscoped alias. Adds isolated regression fixtures for cold-to-warm exclusions, staging directories, enum scoping, declaration visibility and missing reference destinations. --- docs/generation/conf.py | 2 + docs/generation/extensions/cpp_references.py | 14 ++ docs/generation/tests/test_configuration.py | 62 ++++++++ docs/generation/tests/test_cpp_references.py | 143 +++++++++++++++++++ 4 files changed, 221 insertions(+) create mode 100644 docs/generation/tests/test_configuration.py create mode 100644 docs/generation/tests/test_cpp_references.py diff --git a/docs/generation/conf.py b/docs/generation/conf.py index d4425b43..c7ef8c8f 100644 --- a/docs/generation/conf.py +++ b/docs/generation/conf.py @@ -56,6 +56,8 @@ for path in sorted(directory.iterdir()) if path.name != included ) +# Cold and warm output directories must produce the same Sphinx cache settings. +exclude_patterns = sorted(set(exclude_patterns)) myst_heading_anchors = 6 myst_ref_domains = ["std", "cpp"] nitpicky = True diff --git a/docs/generation/extensions/cpp_references.py b/docs/generation/extensions/cpp_references.py index cb824951..4e894156 100644 --- a/docs/generation/extensions/cpp_references.py +++ b/docs/generation/extensions/cpp_references.py @@ -78,6 +78,20 @@ def __init__(self, directory: Path, repository): if member.get("prot") != "private": qualified = member.findtext("qualifiedname") or "::".join(part for part in (name, member.findtext("name")) if part) self.symbols[plain_name(qualified)].add(target) + if member_kind == "enum": + enum_name = member.findtext("name", "") + scope = qualified.rpartition("::")[0] + for value in member.findall("enumvalue"): + value_name = value.findtext("name", "") + value_id = value.get("id") + if value.get("prot") == "private" or not value_name or not value_id: + continue + value_target = (target[0], value_id) + if enum_name and not enum_name.startswith("@"): + self.symbols[plain_name(qualified + "::" + value_name)].add(value_target) + if member.get("strong") != "yes": + qualified_value = "::".join(part for part in (scope, value_name) if part) + self.symbols[plain_name(qualified_value)].add(value_target) for filename in directory.glob("*.xml"): if filename.name in {"index.xml", "Doxyfile.xml"}: continue diff --git a/docs/generation/tests/test_configuration.py b/docs/generation/tests/test_configuration.py new file mode 100644 index 00000000..55f70fcc --- /dev/null +++ b/docs/generation/tests/test_configuration.py @@ -0,0 +1,62 @@ +"""Configuration regressions without importing Sphinx or generating documentation.""" + +import ast +from fnmatch import fnmatchcase +from pathlib import Path +import tempfile +import unittest + + +CONFIGURATION = Path(__file__).resolve().parents[1] / "conf.py" + + +class ExclusionTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory(prefix="cheryl-docs-configuration-") + self.addCleanup(temporary.cleanup) + self.docs = Path(temporary.name) / "docs" + self.generation = self.docs / "generation" + self.generated = self.generation / "generated" + (self.generated / "api").mkdir(parents=True) + for name in ("conf.py", "build.py", "requirements.txt"): + (self.generation / name).touch() + for name in (".generation.lock", "Doxyfile", "sphinx-warnings.log"): + (self.generated / name).touch() + + def exclusions(self): + tree = ast.parse(CONFIGURATION.read_text(encoding="utf-8"), filename=str(CONFIGURATION)) + statements = [ + statement for statement in tree.body + if any(isinstance(node, ast.Name) and node.id == "exclude_patterns" for node in ast.walk(statement)) + ] + scope = { + "DOCS_ROOT": self.docs, + "GENERATION_ROOT": self.generation, + "GENERATED_ROOT": self.generated, + } + # Evaluate only the real exclusion rules, with all paths in the fixture. + # The rest of conf.py would query Git and configure third-party tools. + exec(compile(ast.Module(body=statements, type_ignores=[]), str(CONFIGURATION), "exec"), scope) + return scope["exclude_patterns"] + + def test_output_directories(self): + cold = self.exclusions() + for name in ("html", "doctrees", "xml", "pycache"): + (self.generated / name).mkdir() + warm = self.exclusions() + + self.assertEqual(cold, warm) + self.assertTrue(any(fnmatchcase("generation/generated/html/old.rst", pattern) for pattern in warm)) + self.assertFalse(any(fnmatchcase("generation/generated/api/class.rst", pattern) for pattern in warm)) + + def test_staging_backup(self): + before = self.exclusions() + (self.generated / ".api-previous").mkdir() + staged = self.exclusions() + + self.assertEqual(before, staged) + self.assertTrue(any(fnmatchcase("generation/generated/.api-previous/old.rst", pattern) for pattern in staged)) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/generation/tests/test_cpp_references.py b/docs/generation/tests/test_cpp_references.py new file mode 100644 index 00000000..1ddfe29c --- /dev/null +++ b/docs/generation/tests/test_cpp_references.py @@ -0,0 +1,143 @@ +"""Doxygen-backed reference regressions without Sphinx or generated output.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path +import sys +import tempfile +import types +import unittest +from unittest.mock import patch +import xml.etree.ElementTree as ET + + +def make_reference(builder, source, document, anchor, content): + return {"source": source, "document": document, "anchor": anchor, "content": content} + + +docutils = types.ModuleType("docutils") +docutils.nodes = types.SimpleNamespace() +sphinx = types.ModuleType("sphinx") +sphinx.__path__ = [] +sphinx_util = types.ModuleType("sphinx.util") +sphinx_util.__path__ = [] +sphinx_nodes = types.ModuleType("sphinx.util.nodes") +sphinx_nodes.make_refnode = make_reference +with patch.object(sys, "dont_write_bytecode", True), patch.dict(sys.modules, { + "docutils": docutils, + "sphinx": sphinx, + "sphinx.util": sphinx_util, + "sphinx.util.nodes": sphinx_nodes, +}): + source = Path(__file__).resolve().parents[1] / "extensions/cpp_references.py" + spec = importlib.util.spec_from_file_location("cheryl_cpp_references", source) + references = importlib.util.module_from_spec(spec) + spec.loader.exec_module(references) + + +class EnumeratorTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory(prefix="cheryl-cpp-references-") + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.index = ET.Element("doxygenindex") + self.compounds = {} + self.environment = types.SimpleNamespace(found_docs=set(), docname="guide") + self.app = types.SimpleNamespace(builder=object()) + self.repository = types.SimpleNamespace(root=self.root) + ET.ElementTree(ET.Element("doxygenconfig")).write(self.root / "Doxyfile.xml", encoding="utf-8") + + def add_enum(self, *, owner="CE::Memory", kind="namespace", name="FitPolicy", scoped=False, + visibility="public", value_visibility="public", value="exact"): + owner_id = kind + owner.replace("::", "_") + if owner_id not in self.compounds: + entry = ET.SubElement(self.index, "compound", kind=kind, refid=owner_id) + ET.SubElement(entry, "name").text = owner + compound = ET.Element("compounddef", id=owner_id, kind=kind) + ET.SubElement(compound, "compoundname").text = owner + self.compounds[owner_id] = compound + compound = self.compounds[owner_id] + enum_id = owner_id + "_" + name + member = ET.SubElement(compound, "memberdef", kind="enum", id=enum_id, + prot=visibility, strong="yes" if scoped else "no") + ET.SubElement(member, "name").text = name + ET.SubElement(member, "qualifiedname").text = owner + "::" + name + value_id = enum_id + "_" + value + enumerator = ET.SubElement(member, "enumvalue", id=value_id, prot=value_visibility) + ET.SubElement(enumerator, "name").text = value + document = "generation/generated/api/" + ("enum_" + enum_id if kind == "namespace" else owner_id) + self.environment.found_docs.add(document) + return document, value_id + + def load(self): + ET.ElementTree(self.index).write(self.root / "index.xml", encoding="utf-8") + for identifier, compound in self.compounds.items(): + document = ET.Element("doxygen") + document.append(compound) + ET.ElementTree(document).write(self.root / (identifier + ".xml"), encoding="utf-8") + return references.CppReferences(self.root, self.repository) + + def resolve(self, index, target): + return index.resolve(self.app, self.environment, + {"refdoc": "guide", "reftarget": target}, "value") + + def assert_target(self, result, target): + self.assertIsNotNone(result) + self.assertEqual((result["document"], result["anchor"]), target) + + def test_unscoped(self): + target = self.add_enum() + index = self.load() + + # Native C++ lookup may have lost an unscoped alias after an incremental + # reread; the fallback still reaches the actual documented enumerator. + self.assert_target(self.resolve(index, "::CE::Memory::exact"), target) + self.assert_target(self.resolve(index, "CE::Memory::FitPolicy::exact"), target) + + def test_scoped(self): + target = self.add_enum(name="Mode", scoped=True, value="Quiet") + index = self.load() + + self.assert_target(self.resolve(index, "CE::Memory::Mode::Quiet"), target) + self.assertIsNone(self.resolve(index, "CE::Memory::Quiet")) + + def test_member_enum(self): + target = self.add_enum(owner="CE::Pool", kind="struct") + index = self.load() + + self.assert_target(self.resolve(index, "CE::Pool::exact"), target) + self.assert_target(self.resolve(index, "CE::Pool::FitPolicy::exact"), target) + + def test_private_enum(self): + self.add_enum(owner="CE::Pool", kind="struct", visibility="private") + index = self.load() + + self.assertIsNone(self.resolve(index, "CE::Pool::exact")) + self.assertIsNone(self.resolve(index, "CE::Pool::FitPolicy::exact")) + + def test_private_value(self): + self.add_enum(value_visibility="private") + index = self.load() + + self.assertIsNone(self.resolve(index, "CE::Memory::exact")) + self.assertIsNone(self.resolve(index, "CE::Memory::FitPolicy::exact")) + + def test_anonymous(self): + target = self.add_enum(name="@0") + index = self.load() + + self.assert_target(self.resolve(index, "CE::Memory::exact"), target) + self.assertIsNone(self.resolve(index, "CE::Memory::@0::exact")) + + def test_missing_page(self): + target = self.add_enum() + self.environment.found_docs.remove(target[0]) + index = self.load() + + self.assertIsNone(self.resolve(index, "CE::Memory::exact")) + self.assertIsNone(self.resolve(index, "CE::Memory::FitPolicy::exact")) + + +if __name__ == "__main__": + unittest.main() From 745948ec357ee8e7d91acd744664d1ba530603d1 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 15:22:13 -0700 Subject: [PATCH 15/98] Adds bounded GitHub Actions cache maintenance Prunes closed-pull-request caches and superseded compiler, dependency and pip snapshots after CI completion and on a daily schedule. Trims remaining caches toward an 8 GiB repository budget while protecting active and recent entries, and retaining useful default-branch snapshots until other candidates are gone. Uses trusted default-branch code with narrowly scoped Actions permissions. Provides manual dry runs and an optional confirmed-closed-PR filter, handles concurrent cache removal, and reports planned cache IDs and storage totals. Adds isolated retention and API fixtures to CI and documents the policy and manual workflow. Python syntax, workflow lint and whitespace checks passed; builds and runtime tests were not run. --- .github/ci/cache_cleanup.py | 269 +++++++++++++++++++ .github/ci/test_cache_cleanup.py | 343 ++++++++++++++++++++++++ .github/workflows/cache-maintenance.yml | 58 ++++ .github/workflows/ci.yml | 4 +- docs/development/building.md | 33 +++ 5 files changed, 706 insertions(+), 1 deletion(-) create mode 100644 .github/ci/cache_cleanup.py create mode 100644 .github/ci/test_cache_cleanup.py create mode 100644 .github/workflows/cache-maintenance.yml diff --git a/.github/ci/cache_cleanup.py b/.github/ci/cache_cleanup.py new file mode 100644 index 00000000..a42ace8b --- /dev/null +++ b/.github/ci/cache_cleanup.py @@ -0,0 +1,269 @@ +"""Plan or apply bounded GitHub Actions cache maintenance; dry run by default.""" + +import argparse +from datetime import datetime, timedelta, timezone +import json +import math +import os +from pathlib import Path +import re +import sys +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode +from urllib.request import HTTPRedirectHandler, Request, build_opener + + +GIB = 1024 ** 3 +SNAPSHOT = re.compile(r"^((?:ccache-v1|dependencies-v1|pip-v1)-.+)-[0-9]+-[0-9]+$") +PULL_REF = re.compile(r"^refs/pull/([1-9][0-9]*)/merge$") +KNOWN_PREFIXES = ("ccache-v1-", "dependencies-v1-", "pip-v1-", "docs-tools-v1-", + "windows-tools-v1-", "submodule-git-v1-", "apt-debs-v1-") +MAINTENANCE_PATH = ".github/workflows/cache-maintenance.yml" + + +def lineage(key): + match = SNAPSHOT.fullmatch(key) + return match.group(1) if match else None + + +def timestamp(value): + if not isinstance(value, str): + raise ValueError("Cache timestamps must be ISO 8601 strings") + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + if parsed.tzinfo is None: + raise ValueError("Cache timestamps must include a timezone") + return parsed + + +def plan_caches(caches, *, default_branch, closed_prs, active_refs, now, + budget_bytes=8 * GIB, grace_seconds=900, closed_pr=None, unresolved_refs=frozenset()): + """Return a deletion plan without mutating caches or accessing GitHub. + + Active/unresolved refs and recently created or accessed entries are protected. + Exact-PR mode never compacts or trims entries outside that confirmed closed PR. + """ + if not isinstance(default_branch, str) or not default_branch or now.tzinfo is None: + raise ValueError("A default branch and timezone-aware current time are required") + if type(budget_bytes) is not int or budget_bytes < 0 or grace_seconds < 0: + raise ValueError("The cache budget and grace period must be nonnegative") + if closed_pr is not None and (type(closed_pr) is not int or closed_pr <= 0): + raise ValueError("A pull request number must be positive") + + entries = [] + seen = set() + protected = set() + cutoff = now - timedelta(seconds=grace_seconds) + for cache in caches: + identifier = cache.get("id") + if type(identifier) is not int or identifier <= 0 or identifier in seen: + raise ValueError("Cache IDs must be unique positive integers") + seen.add(identifier) + if type(cache.get("size_in_bytes")) is not int or cache["size_in_bytes"] < 0: + raise ValueError("Cache sizes must be nonnegative integers") + if any(not isinstance(cache.get(key), str) or not cache[key] for key in ("key", "ref", "version")): + raise ValueError("Each cache needs a key, ref and version") + created, accessed = timestamp(cache["created_at"]), timestamp(cache["last_accessed_at"]) + record = (dict(cache), created, accessed, lineage(cache["key"])) + entries.append(record) + if cache["ref"] in active_refs or cache["ref"] in unresolved_refs or max(created, accessed) >= cutoff: + protected.add(identifier) + + before = sum(cache["size_in_bytes"] for cache, *_ in entries) + result = {"before_bytes": before, "after_bytes": before, + "protected_bytes": sum(cache["size_in_bytes"] for cache, *_ in entries if cache["id"] in protected), + "protected_ids": sorted(protected), "deletions": []} + selected = set() + + def remove(cache, reason): + if cache["id"] not in protected and cache["id"] not in selected: + selected.add(cache["id"]) + result["deletions"].append({**cache, "reason": reason}) + result["after_bytes"] -= cache["size_in_bytes"] + + for cache, *_ in entries: + match = PULL_REF.fullmatch(cache["ref"]) + number = int(match.group(1)) if match else None + if number in closed_prs and (closed_pr is None or number == closed_pr): + remove(cache, "closed pull request") + if closed_pr is not None: + return result + + newest = {} + baseline = {} + for record in entries: + cache, created, _, logical = record + if logical is not None: + bucket = (cache["ref"], cache["version"], logical) + if bucket not in newest or (created, cache["id"]) > (newest[bucket][1], newest[bucket][0]["id"]): + newest[bucket] = record + if cache["ref"] == "refs/heads/" + default_branch and cache["key"].startswith(KNOWN_PREFIXES): + bucket = (cache["version"], logical or cache["key"]) + if bucket not in baseline or (created, cache["id"]) > (baseline[bucket][1], baseline[bucket][0]["id"]): + baseline[bucket] = record + for cache, _, _, logical in entries: + if logical is not None and newest[(cache["ref"], cache["version"], logical)][0]["id"] != cache["id"]: + remove(cache, "superseded snapshot") + + baseline_ids = {record[0]["id"] for record in baseline.values()} + for cache, _, _, _ in sorted(entries, key=lambda record: ( + record[0]["id"] in baseline_ids, record[2], record[1], record[0]["id"])): + if result["after_bytes"] <= budget_bytes: + break + remove(cache, "default-branch baseline" if cache["id"] in baseline_ids else "least recently used") + return result + + +class NoRedirect(HTTPRedirectHandler): + def redirect_request(self, request, file, code, message, headers, new_url): + return None + + +class GitHubApi: + def __init__(self, repository, token, api_url="https://api.github.com"): + if not isinstance(repository, str) or not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository) or any( + part in {".", ".."} for part in repository.split("/")): + raise ValueError("GITHUB_REPOSITORY must be owner/repository") + if not isinstance(api_url, str) or api_url.rstrip("/") != "https://api.github.com": + raise ValueError("Cache maintenance supports the GitHub.com HTTPS API only") + if not isinstance(token, str) or not token or any(character.isspace() for character in token): + raise ValueError("A nonempty GitHub token without whitespace is required") + self.base = api_url.rstrip("/") + "/repos/" + repository + self.token = token + self.opener = build_opener(NoRedirect()) + + def request(self, method, path, *, missing_ok=False, **query): + if method not in {"GET", "DELETE"} or (path and not path.startswith("/")) or any( + value in path for value in ("//", "?", "#")) or ".." in path.split("/"): + raise ValueError("Expected a repository-relative GitHub cache API request") + url = self.base + path + ("?" + urlencode(query) if query else "") + request = Request(url, method=method, headers={ + "Accept": "application/vnd.github+json", "Authorization": "Bearer " + self.token, + "X-GitHub-Api-Version": "2026-03-10", "User-Agent": "cheryl-cache-maintenance", + }) + try: + with self.opener.open(request, timeout=30) as response: + data = response.read() + except HTTPError as error: + if error.code == 404 and missing_ok: + return None + raise RuntimeError(f"GitHub API {method} {path} returned HTTP {error.code}") from None + except URLError: + raise RuntimeError(f"GitHub API {method} {path} could not be reached") from None + value = json.loads(data) if data else {} + if not isinstance(value, dict): + raise ValueError("The GitHub API returned an unexpected response") + return value + + def pages(self, path, field, **query): + entries = [] + for page in range(1, 1001): + value = self.request("GET", path, per_page=100, page=page, **query) + batch = value.get(field) + if not isinstance(batch, list): + raise ValueError(f"The GitHub API response is missing {field}") + entries.extend(batch) + if len(batch) < 100: + return entries + raise RuntimeError("GitHub API pagination exceeded the maintenance limit") + + +def active_refs(api, caches): + refs = set() + current_run = os.environ.get("GITHUB_RUN_ID") + for status in ("queued", "in_progress", "waiting", "pending", "requested"): + for run in api.pages("/actions/runs", "workflow_runs", status=status): + if (str(run["id"]) == current_run or run.get("path", "").split("@", 1)[0] == MAINTENANCE_PATH + or run.get("name") == "Maintain Actions caches"): + continue + if run.get("head_branch"): + refs.add("refs/heads/" + run["head_branch"]) + refs.add("refs/tags/" + run["head_branch"]) + pulls = run.get("pull_requests", []) + if not pulls and (run.get("event") == "pull_request" or not run.get("head_branch")): + # Fork runs can omit PR metadata; other runs can omit their ref. + # Keep every possible scope when the API cannot identify one. + refs.update(cache["ref"] for cache in caches) + for pull in pulls: + refs.add(f"refs/pull/{int(pull['number'])}/merge") + if pull.get("base", {}).get("ref"): + refs.add("refs/heads/" + pull["base"]["ref"]) + return refs + + +def pull_states(api, caches, closed_pr=None): + numbers = {closed_pr} if closed_pr is not None else { + int(match.group(1)) for cache in caches if (match := PULL_REF.fullmatch(cache["ref"])) + } + closed, unresolved = set(), set() + for number in sorted(numbers): + pull = api.request("GET", f"/pulls/{number}", missing_ok=True) + if pull is None: + unresolved.add(f"refs/pull/{number}/merge") + elif pull.get("state") == "closed": + closed.add(number) + elif pull.get("state") != "open": + raise ValueError("The GitHub API returned an unexpected pull request state") + return closed, unresolved + + +def apply_plan(api, plan, *, apply=False): + result = {"dry_run": not apply, "deleted_ids": [], "missing_ids": []} + if apply: + for cache in plan["deletions"]: + response = api.request("DELETE", f"/actions/caches/{cache['id']}", missing_ok=True) + result["missing_ids" if response is None else "deleted_ids"].append(cache["id"]) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--apply", action="store_true", help="Delete the planned caches; otherwise report only") + parser.add_argument("--budget-gib", type=float, default=8, help="Repository cache target in GiB (default: 8)") + parser.add_argument("--closed-pr", type=int, help="Only remove caches belonging to this confirmed closed PR") + arguments = parser.parse_args() + if not math.isfinite(arguments.budget_gib) or arguments.budget_gib < 0: + parser.error("The cache budget must be finite and nonnegative") + if arguments.closed_pr is not None and arguments.closed_pr <= 0: + parser.error("The pull request number must be positive") + budget_bytes = int(arguments.budget_gib * GIB) + + api = GitHubApi(os.environ.get("GITHUB_REPOSITORY"), os.environ.get("GITHUB_TOKEN"), + os.environ.get("GITHUB_API_URL", "https://api.github.com")) + repository = api.request("GET", "") + caches = api.pages("/actions/caches", "actions_caches", sort="created_at", direction="asc") + closed, unresolved = pull_states(api, caches, arguments.closed_pr) + if arguments.closed_pr is not None and arguments.closed_pr not in closed: + print("The requested pull request is not confirmed closed; no caches selected.") + return 0 + # Check live workflow admission after gathering cache/PR metadata. A new run + # can still race deletion; cache maintenance offers no atomic restore lease. + running = active_refs(api, caches) + plan = plan_caches(caches, default_branch=repository["default_branch"], closed_prs=closed, + active_refs=running, unresolved_refs=unresolved, now=datetime.now(timezone.utc), + budget_bytes=budget_bytes, closed_pr=arguments.closed_pr) + print("Applying cache maintenance." if arguments.apply else "Dry run; pass --apply to delete these caches.") + for cache in plan["deletions"]: + print(json.dumps({key: cache[key] for key in ("id", "key", "ref", "size_in_bytes", "reason")})) + summary = (f"Caches: {len(caches)}; selected: {len(plan['deletions'])}; " + f"storage: {plan['before_bytes'] / GIB:.2f} GiB ({plan['before_bytes']} bytes) -> " + f"{plan['after_bytes'] / GIB:.2f} GiB ({plan['after_bytes']} bytes); " + f"budget: {budget_bytes / GIB:.2f} GiB ({budget_bytes} bytes); " + f"protected: {plan['protected_bytes'] / GIB:.2f} GiB ({plan['protected_bytes']} bytes)") + print(summary) + if arguments.closed_pr is None and plan["after_bytes"] > budget_bytes: + print("The target cannot be reached while active, recent or unresolved caches remain protected.") + result = apply_plan(api, plan, apply=arguments.apply) + if arguments.apply: + print(f"Deleted: {len(result['deleted_ids'])}; already absent: {len(result['missing_ids'])}") + if os.environ.get("GITHUB_STEP_SUMMARY"): + with Path(os.environ["GITHUB_STEP_SUMMARY"]).open("a", encoding="utf-8") as output: + output.write("### Cache maintenance" + (" (dry run)" if result["dry_run"] else "") + "\n\n" + summary + "\n") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (KeyError, OSError, ValueError, RuntimeError) as error: + print(f"Cache maintenance failed: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/.github/ci/test_cache_cleanup.py b/.github/ci/test_cache_cleanup.py new file mode 100644 index 00000000..fe45e8e4 --- /dev/null +++ b/.github/ci/test_cache_cleanup.py @@ -0,0 +1,343 @@ +"""Check cache retention decisions and API behavior without network access.""" + +from contextlib import redirect_stderr, redirect_stdout +from datetime import datetime, timedelta, timezone +import importlib.util +import io +import os +from pathlib import Path +import unittest +from unittest.mock import Mock, call, patch +from urllib.error import HTTPError + + +spec = importlib.util.spec_from_file_location( + "cache_cleanup", Path(__file__).with_name("cache_cleanup.py") +) +cleanup = importlib.util.module_from_spec(spec) +spec.loader.exec_module(cleanup) + +NOW = datetime(2026, 10, 15, 12, tzinfo=timezone.utc) +GIB = 1024 ** 3 +COMPILER = "ccache-v1-ubuntu-24.04-X64-linux-gcc-hwloc-Release" + + +def cache(identifier, key=None, *, ref="refs/heads/develop", version="v1", + size=100, age=21600, accessed=3600): + def timestamp(seconds): + return (NOW - timedelta(seconds=seconds)).isoformat().replace("+00:00", "Z") + + return { + "id": identifier, + "key": key or f"{COMPILER}-{identifier}-1", + "ref": ref, + "version": version, + "size_in_bytes": size, + "created_at": timestamp(age), + "last_accessed_at": timestamp(accessed), + } + + +def plan(caches, **options): + return cleanup.plan_caches(caches, **{ + "default_branch": "develop", + "closed_prs": set(), + "active_refs": set(), + "now": NOW, + **options, + }) + + +def removed(result): + return {row["id"]: row["reason"] for row in result["deletions"]} + + +class PlanTests(unittest.TestCase): + def test_empty(self): + result = plan([]) + self.assertEqual(removed(result), {}) + self.assertEqual(result["before_bytes"], 0) + self.assertEqual(result["after_bytes"], 0) + + def test_lineages(self): + for key, expected in ( + (f"{COMPILER}-12345-2", COMPILER), + ("dependencies-v1-Windows-X64-modules-abcd-12345-2", + "dependencies-v1-Windows-X64-modules-abcd"), + ("pip-v1-Windows-X64-cpython-312-abcd-12345-2", + "pip-v1-Windows-X64-cpython-312-abcd"), + ): + with self.subTest(key=key): + self.assertEqual(cleanup.lineage(key), expected) + for key in ( + "windows-tools-v1-Windows-X64-baseline-ccache-4.14.0", + "submodule-git-v1-Linux-X64-12345-2", + "apt-debs-v1-Linux-X64-12345-2", + "docs-tools-v1-Linux-X64-12345-2", + "unrelated-12345-2", + f"{COMPILER}-12345-retry", + ): + with self.subTest(key=key): + self.assertIsNone(cleanup.lineage(key)) + + def test_latest(self): + older = cache(99, age=18000, accessed=1800) + newer = cache(1, age=7200, accessed=3600) + result = plan([older, newer]) + self.assertEqual(removed(result), {99: "superseded snapshot"}) + self.assertEqual(result["after_bytes"], newer["size_in_bytes"]) + + def test_tie(self): + result = plan([cache(1), cache(2)]) + self.assertEqual(removed(result), {1: "superseded snapshot"}) + + def test_isolation(self): + rows = [ + cache(1), + cache(2, ref="refs/heads/main"), + cache(3, version="v2"), + cache(4, key="ccache-v1-ubuntu-24.04-X64-linux-clang-debug-Debug-4-1"), + ] + self.assertEqual(removed(plan(rows)), {}) + + def test_immutable(self): + rows = [cache(1, key="apt-debs-v1-Linux-X64-123-1"), + cache(2, key="apt-debs-v1-Linux-X64-456-1")] + self.assertEqual(removed(plan(rows)), {}) + + def test_closed(self): + rows = [cache(1, ref="refs/pull/3/merge"), + cache(2, ref="refs/pull/30/merge"), + cache(3, ref="refs/heads/pull/3/merge"), + cache(4), cache(5)] + result = plan(rows, closed_prs={3}, closed_pr=3) + self.assertEqual(removed(result), {1: "closed pull request"}) + self.assertEqual(result["before_bytes"], 500) + self.assertEqual(result["after_bytes"], 400) + + def test_closed_only(self): + rows = [cache(1, ref="refs/pull/3/merge"), + cache(2, ref="refs/pull/4/merge", size=10 * GIB)] + result = plan(rows, closed_prs={3, 4}, closed_pr=3) + self.assertEqual(removed(result), {1: "closed pull request"}) + + def test_unconfirmed(self): + result = plan([cache(1, ref="refs/pull/3/merge")], closed_pr=3) + self.assertEqual(removed(result), {}) + + def test_open(self): + self.assertEqual(removed(plan([cache(1, ref="refs/pull/3/merge")])), {}) + + def test_budget(self): + rows = [cache(1, key="other-old", ref="refs/pull/1/merge", + size=3 * GIB, accessed=18000), + cache(2, key="other-middle", ref="refs/pull/2/merge", + size=3 * GIB, accessed=10800), + cache(3, key="other-new", ref="refs/pull/3/merge", + size=3 * GIB, accessed=3600)] + result = plan(rows) + self.assertEqual(removed(result), {1: "least recently used"}) + self.assertEqual(result["after_bytes"], 6 * GIB) + + def test_baseline(self): + rows = [cache(1, size=5 * GIB, accessed=18000), + cache(2, ref="refs/pull/3/merge", size=4 * GIB, accessed=3600)] + result = plan(rows) + self.assertEqual(removed(result), {2: "least recently used"}) + self.assertEqual(result["after_bytes"], 5 * GIB) + + def test_large_baseline(self): + result = plan([cache(1, size=9 * GIB)]) + self.assertEqual(removed(result), {1: "default-branch baseline"}) + self.assertEqual(result["after_bytes"], 0) + + def test_active(self): + rows = [cache(1, size=5 * GIB), cache(2, size=4 * GIB)] + result = plan(rows, active_refs={"refs/heads/develop"}) + self.assertEqual(removed(result), {}) + self.assertEqual(set(result["protected_ids"]), {1, 2}) + self.assertEqual(result["protected_bytes"], 9 * GIB) + + def test_recent(self): + row = cache(1, ref="refs/pull/3/merge", age=300, accessed=60) + result = plan([row], closed_prs={3}) + self.assertEqual(removed(result), {}) + self.assertIn(1, result["protected_ids"]) + + def test_recent_access(self): + result = plan([cache(1, ref="refs/pull/3/merge", accessed=60)], closed_prs={3}) + self.assertEqual(removed(result), {}) + self.assertIn(1, result["protected_ids"]) + + def test_closed_active(self): + result = plan([cache(1, ref="refs/pull/3/merge")], closed_prs={3}, + closed_pr=3, active_refs={"refs/pull/3/merge"}) + self.assertEqual(removed(result), {}) + + def test_unresolved(self): + result = plan([cache(1, ref="refs/pull/3/merge", size=9 * GIB)], + closed_prs={3}, unresolved_refs={"refs/pull/3/merge"}) + self.assertEqual(removed(result), {}) + self.assertIn(1, result["protected_ids"]) + + def test_copy(self): + rows = [cache(1), cache(2)] + original = [dict(row) for row in rows] + plan(rows) + self.assertEqual(rows, original) + + def test_invalid(self): + for update in ({"id": "invalid"}, {"size_in_bytes": -1}, + {"created_at": "invalid"}, {"last_accessed_at": "invalid"}): + with self.subTest(update=update), self.assertRaises((ValueError, RuntimeError)): + plan([{**cache(1), **update}]) + + +class ApiTests(unittest.TestCase): + def test_dry_run(self): + api = Mock(spec=cleanup.GitHubApi) + with redirect_stdout(io.StringIO()): + result = cleanup.apply_plan(api, plan([cache(1), cache(2)])) + api.request.assert_not_called() + self.assertEqual(result["deleted_ids"], []) + self.assertTrue(result["dry_run"]) + + def test_noop(self): + for rows in ([], [cache(1)]): + with self.subTest(rows=rows): + api = Mock(spec=cleanup.GitHubApi) + with redirect_stdout(io.StringIO()): + result = cleanup.apply_plan(api, plan(rows), apply=True) + api.request.assert_not_called() + self.assertEqual(result["deleted_ids"], []) + + def test_delete(self): + api = Mock(spec=cleanup.GitHubApi) + api.request.return_value = {} + with redirect_stdout(io.StringIO()): + result = cleanup.apply_plan(api, plan([cache(1), cache(2)]), apply=True) + api.request.assert_called_once_with("DELETE", "/actions/caches/1", missing_ok=True) + self.assertEqual(result["deleted_ids"], [1]) + self.assertEqual(result["missing_ids"], []) + self.assertFalse(result["dry_run"]) + + def test_missing(self): + api = Mock(spec=cleanup.GitHubApi) + api.request.return_value = None + with redirect_stdout(io.StringIO()): + result = cleanup.apply_plan(api, plan([cache(1), cache(2)]), apply=True) + self.assertEqual(result["deleted_ids"], []) + self.assertEqual(result["missing_ids"], [1]) + + def test_pages(self): + api = cleanup.GitHubApi("cppcooper/cheryl-engine", "fixture-token") + first = [{"id": identifier} for identifier in range(100)] + last = [{"id": 100}] + api.request = Mock(side_effect=[{"actions_caches": first}, {"actions_caches": last}]) + self.assertEqual(api.pages("/actions/caches", "actions_caches"), first + last) + self.assertEqual(api.request.call_args_list, [ + call("GET", "/actions/caches", per_page=100, page=1), + call("GET", "/actions/caches", per_page=100, page=2), + ]) + + def test_http_missing(self): + api = cleanup.GitHubApi("cppcooper/cheryl-engine", "fixture-token") + error = HTTPError("https://api.github.com", 404, "Not Found", {}, io.BytesIO()) + self.addCleanup(error.close) + api.opener = Mock() + api.opener.open.side_effect = error + self.assertIsNone(api.request("DELETE", "/actions/caches/1", missing_ok=True)) + with self.assertRaisesRegex(RuntimeError, "HTTP 404"): + api.request("DELETE", "/actions/caches/1") + + +class StateTests(unittest.TestCase): + def runs(self, *records, status="in_progress"): + api = Mock(spec=cleanup.GitHubApi) + api.pages.side_effect = lambda path, field, **query: list(records) if query["status"] == status else [] + return api + + def test_pull_states(self): + api = Mock(spec=cleanup.GitHubApi) + api.request.side_effect = [None, {"state": "open"}] + rows = [cache(1, ref="refs/pull/3/merge"), cache(2, ref="refs/pull/30/merge"), + cache(3, ref="refs/pull/3/merge")] + closed, unresolved = cleanup.pull_states(api, rows) + self.assertEqual(closed, set()) + self.assertEqual(unresolved, {"refs/pull/3/merge"}) + self.assertEqual(api.request.call_args_list, [ + call("GET", "/pulls/3", missing_ok=True), + call("GET", "/pulls/30", missing_ok=True), + ]) + result = plan(rows, closed_prs=closed, unresolved_refs=unresolved) + self.assertEqual(removed(result), {}) + self.assertEqual(set(result["protected_ids"]), {1, 3}) + + def test_active_pull(self): + api = self.runs({"id": 1, "head_branch": "feature", "event": "pull_request", + "pull_requests": [{"number": 3, "base": {"ref": "develop"}}]}) + with patch.dict(os.environ, {}, clear=True): + refs = cleanup.active_refs(api, []) + self.assertTrue({"refs/heads/feature", "refs/pull/3/merge", "refs/heads/develop"} <= refs) + + def test_empty_pulls(self): + rows = [cache(1), cache(2, ref="refs/pull/3/merge"), cache(3, ref="refs/heads/main")] + for metadata in ({}, {"pull_requests": []}): + with self.subTest(metadata=metadata): + api = self.runs({"id": 1, "event": "pull_request", "head_branch": "feature", **metadata}) + with patch.dict(os.environ, {}, clear=True): + refs = cleanup.active_refs(api, rows) + self.assertTrue({row["ref"] for row in rows} <= refs) + self.assertEqual(removed(plan(rows, active_refs=refs)), {}) + + def test_active_tag(self): + api = self.runs({"id": 1, "head_branch": "v4.0.0", "event": "push", "pull_requests": []}) + rows = [cache(1, ref="refs/tags/v4.0.0", size=9 * GIB)] + with patch.dict(os.environ, {}, clear=True): + refs = cleanup.active_refs(api, rows) + self.assertIn("refs/tags/v4.0.0", refs) + self.assertEqual(removed(plan(rows, active_refs=refs)), {}) + + def test_missing_ref(self): + rows = [cache(1), cache(2), cache(3, ref="refs/pull/3/merge")] + api = self.runs({"id": 1, "event": "push", "head_branch": None, "pull_requests": []}, + status="waiting") + with patch.dict(os.environ, {}, clear=True): + refs = cleanup.active_refs(api, rows) + self.assertTrue({row["ref"] for row in rows} <= refs) + self.assertEqual(removed(plan(rows, active_refs=refs)), {}) + + def test_maintenance(self): + api = self.runs( + {"id": 1, "head_branch": "develop", "path": ".github/workflows/cache-maintenance.yml@develop"}, + {"id": 2, "head_branch": "main", "name": "Maintain Actions caches"}, + {"id": 3, "head_branch": "current-run"}, + status="queued", + ) + with patch.dict(os.environ, {"GITHUB_RUN_ID": "3"}, clear=True): + self.assertEqual(cleanup.active_refs(api, []), set()) + + def test_unconfirmed_main(self): + api = Mock(spec=cleanup.GitHubApi) + api.request.side_effect = [{"default_branch": "develop"}, None] + api.pages.return_value = [cache(1, ref="refs/pull/3/merge")] + with patch.object(cleanup, "GitHubApi", return_value=api), \ + patch.object(cleanup.sys, "argv", ["cache_cleanup.py", "--apply", "--closed-pr", "3"]), \ + patch.dict(os.environ, {}, clear=True), redirect_stdout(io.StringIO()): + self.assertEqual(cleanup.main(), 0) + self.assertEqual(api.request.call_args_list, [ + call("GET", ""), call("GET", "/pulls/3", missing_ok=True), + ]) + + def test_invalid_pr(self): + for number in ("0", "-1", "invalid"): + with self.subTest(number=number), patch.object(cleanup, "GitHubApi") as api, \ + patch.object(cleanup.sys, "argv", ["cache_cleanup.py", "--closed-pr", number]), \ + redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as stopped: + cleanup.main() + self.assertEqual(stopped.exception.code, 2) + api.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/cache-maintenance.yml b/.github/workflows/cache-maintenance.yml new file mode 100644 index 00000000..f22ec63a --- /dev/null +++ b/.github/workflows/cache-maintenance.yml @@ -0,0 +1,58 @@ +name: Maintain Actions caches + +on: + schedule: + - cron: '23 4 * * *' + workflow_run: + workflows: ['Build and test', 'Documentation'] + types: [completed] + workflow_dispatch: + inputs: + dry_run: + description: 'Preview deletions without changing caches' + type: boolean + default: true + closed_pr: + description: 'Optional closed PR number; leave empty for repository cleanup' + type: string + default: '' + +permissions: + contents: read + +concurrency: + group: actions-cache-maintenance + cancel-in-progress: false + +jobs: + cleanup: + if: github.repository == 'cppcooper/cheryl-engine' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + actions: write + pull-requests: read + env: + PYTHONDONTWRITEBYTECODE: '1' + steps: + # Elevated events use trusted code, never the triggering PR or workflow's head. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: refs/heads/${{ github.event.repository.default_branch }} + persist-credentials: false + - name: Maintain cache budget + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + APPLY_CLEANUP: ${{ github.event_name != 'workflow_dispatch' || !inputs.dry_run }} + CLOSED_PR: ${{ inputs.closed_pr || '' }} + run: | + args=(--budget-gib 8) + if [[ "$APPLY_CLEANUP" == 'true' ]]; then + args+=(--apply) + fi + if [[ -n "$CLOSED_PR" ]]; then + args+=(--closed-pr "$CLOSED_PR") + fi + python3 .github/ci/cache_cleanup.py "${args[@]}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f81d8081..60a73e72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,9 @@ jobs: id: ccache-ready run: ccache --zero-stats - name: Check CI tools - run: python .github/ci/test_ci.py + run: | + python .github/ci/test_ci.py + python .github/ci/test_cache_cleanup.py - name: Build and test Linux if: runner.os == 'Linux' env: diff --git a/docs/development/building.md b/docs/development/building.md index b11c4dcc..3bca1906 100644 --- a/docs/development/building.md +++ b/docs/development/building.md @@ -365,6 +365,39 @@ SDK consumers are accepted. The first main-push artifact collection remains [TR20](../testing-requests.md#tr20-automated-main-push-sdk-collection); collection is intentionally skipped on PR, `develop` and manual runs. +### Actions cache retention + +Compiler and dependency caches speed up subsequent CI runs. They are separate +from the SDK and diagnostic artifacts above. `CCACHE_MAXSIZE=1G` limits each +job's compiler cache, rather than the repository's total Actions cache storage. +Compiler, Windows dependency and pip caches use new run-specific keys so their +contents can grow between builds: GitHub caches are immutable after saving. + +[cache-maintenance.yml](../../.github/workflows/cache-maintenance.yml) runs after +the build or documentation workflow finishes and daily at 04:23 UTC. It removes +caches belonging to closed pull requests, keeps the newest compiler/dependency/pip +snapshot per branch, configuration and cache version, and trims older caches +toward an 8 GiB repository budget. Active workflow refs and caches created or +accessed within the last 15 minutes are protected. The newest known default-branch +snapshots are evicted last if the budget requires it. The budget is a target: +concurrent uploads and protected caches can temporarily keep usage above it. +Cleanup deletes cached build inputs, without deleting SDK artifacts or changing +build results; an evicted cache is regenerated when next needed. + +To inspect or apply cleanup, open **Actions → Maintain Actions caches → Run +workflow** after this workflow has reached the repository's default branch. +Leave **dry_run** enabled to preview cache IDs, keys, refs, sizes and deletion +reasons. Optionally enter a closed PR number in **closed_pr** to restrict the +operation to that PR's caches. Review the run's report, then dispatch again with +**dry_run** disabled to apply the current plan. Automatic runs apply cleanup; +they execute the default branch's script without loading PR code, artifacts or +cached executables. + +GitHub also [expires unused caches and evicts old entries at its storage +limit](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#usage-limits-and-eviction-policy). +The maintenance policy removes redundant snapshots before that limit causes +frequent replacement of useful caches. + ### Hosted Windows coverage limits Both Windows profiles leave these focused cases unaccepted: From d19083b42163170b193d4edfc243ebce796481d1 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 15:22:38 -0700 Subject: [PATCH 16/98] Closes cache API error responses before handling failures Release HTTP response streams during missing-cache and missing-pull-request handling while retaining the status needed for sanitized diagnostics. --- .github/ci/cache_cleanup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/ci/cache_cleanup.py b/.github/ci/cache_cleanup.py index a42ace8b..2304b137 100644 --- a/.github/ci/cache_cleanup.py +++ b/.github/ci/cache_cleanup.py @@ -144,9 +144,11 @@ def request(self, method, path, *, missing_ok=False, **query): with self.opener.open(request, timeout=30) as response: data = response.read() except HTTPError as error: - if error.code == 404 and missing_ok: + status = error.code + error.close() + if status == 404 and missing_ok: return None - raise RuntimeError(f"GitHub API {method} {path} returned HTTP {error.code}") from None + raise RuntimeError(f"GitHub API {method} {path} returned HTTP {status}") from None except URLError: raise RuntimeError(f"GitHub API {method} {path} could not be reached") from None value = json.loads(data) if data else {} From 6f08d46f87441b92e5673724ca174fc9bdd681a9 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 15:38:57 -0700 Subject: [PATCH 17/98] Prioritizes main, develop and open pull request compiler caches Preserve each branch and profile independently during compaction, then evict ordinary entries before open-PR, develop and main baselines when the repository budget requires it. Only confirmed open PRs receive that priority; live/recent/unresolved protections remain intact. Recognize layered v2 snapshots and retain v1 branch priority until a matching replacement exists. Extend isolated policy fixtures for both branch baselines, quota tiers, PR lookup and format migration. Static syntax and whitespace checks passed; tests were not run. --- .github/ci/cache_cleanup.py | 47 ++++++++++++--- .github/ci/test_cache_cleanup.py | 99 +++++++++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 10 deletions(-) diff --git a/.github/ci/cache_cleanup.py b/.github/ci/cache_cleanup.py index 2304b137..654cf28a 100644 --- a/.github/ci/cache_cleanup.py +++ b/.github/ci/cache_cleanup.py @@ -14,9 +14,9 @@ GIB = 1024 ** 3 -SNAPSHOT = re.compile(r"^((?:ccache-v1|dependencies-v1|pip-v1)-.+)-[0-9]+-[0-9]+$") +SNAPSHOT = re.compile(r"^((?:ccache-v[12]|dependencies-v1|pip-v1)-.+)-[0-9]+-[0-9]+$") PULL_REF = re.compile(r"^refs/pull/([1-9][0-9]*)/merge$") -KNOWN_PREFIXES = ("ccache-v1-", "dependencies-v1-", "pip-v1-", "docs-tools-v1-", +KNOWN_PREFIXES = ("ccache-v1-", "ccache-v2-", "dependencies-v1-", "pip-v1-", "docs-tools-v1-", "windows-tools-v1-", "submodule-git-v1-", "apt-debs-v1-") MAINTENANCE_PATH = ".github/workflows/cache-maintenance.yml" @@ -36,11 +36,14 @@ def timestamp(value): def plan_caches(caches, *, default_branch, closed_prs, active_refs, now, - budget_bytes=8 * GIB, grace_seconds=900, closed_pr=None, unresolved_refs=frozenset()): + budget_bytes=8 * GIB, grace_seconds=900, closed_pr=None, unresolved_refs=frozenset(), + open_prs=frozenset()): """Return a deletion plan without mutating caches or accessing GitHub. Active/unresolved refs and recently created or accessed entries are protected. Exact-PR mode never compacts or trims entries outside that confirmed closed PR. + Newest compiler snapshots prefer main, develop, then confirmed open PRs when + the budget requires eviction; these priorities do not reserve storage. """ if not isinstance(default_branch, str) or not default_branch or now.tzinfo is None: raise ValueError("A default branch and timezone-aware current time are required") @@ -88,28 +91,49 @@ def remove(cache, reason): if closed_pr is not None: return result + layered_profiles = { + (cache["ref"], "ccache-v1-" + logical[len("ccache-v2-"):].rsplit("-", 1)[0]) + for cache, _, _, logical in entries + if logical is not None and logical.startswith("ccache-v2-") + and logical.rsplit("-", 1)[1] in {"main", "develop", "pr", "branch"} + } newest = {} baseline = {} + baseline_policy = {} for record in entries: cache, created, _, logical = record if logical is not None: bucket = (cache["ref"], cache["version"], logical) if bucket not in newest or (created, cache["id"]) > (newest[bucket][1], newest[bucket][0]["id"]): newest[bucket] = record - if cache["ref"] == "refs/heads/" + default_branch and cache["key"].startswith(KNOWN_PREFIXES): - bucket = (cache["version"], logical or cache["key"]) + priority, reason = 0, "least recently used" + replaced = cache["key"].startswith("ccache-v1-") and (cache["ref"], logical) in layered_profiles + if not replaced: + if cache["ref"] == "refs/heads/" + default_branch and cache["key"].startswith(KNOWN_PREFIXES): + priority, reason = 2, "default-branch baseline" + if logical is not None and cache["key"].startswith(("ccache-v1-", "ccache-v2-")): + match = PULL_REF.fullmatch(cache["ref"]) + if cache["ref"] == "refs/heads/main": + priority, reason = 3, "main baseline" + elif cache["ref"] == "refs/heads/develop": + priority, reason = 2, "develop baseline" + elif match and int(match.group(1)) in open_prs: + priority, reason = 1, "open pull request baseline" + if priority: + bucket = (cache["ref"], cache["version"], logical or cache["key"]) if bucket not in baseline or (created, cache["id"]) > (baseline[bucket][1], baseline[bucket][0]["id"]): baseline[bucket] = record + baseline_policy[bucket] = (priority, reason) for cache, _, _, logical in entries: if logical is not None and newest[(cache["ref"], cache["version"], logical)][0]["id"] != cache["id"]: remove(cache, "superseded snapshot") - baseline_ids = {record[0]["id"] for record in baseline.values()} + priorities = {record[0]["id"]: baseline_policy[bucket] for bucket, record in baseline.items()} for cache, _, _, _ in sorted(entries, key=lambda record: ( - record[0]["id"] in baseline_ids, record[2], record[1], record[0]["id"])): + priorities.get(record[0]["id"], (0, ""))[0], record[2], record[1], record[0]["id"])): if result["after_bytes"] <= budget_bytes: break - remove(cache, "default-branch baseline" if cache["id"] in baseline_ids else "least recently used") + remove(cache, priorities.get(cache["id"], (0, "least recently used"))[1]) return result @@ -237,12 +261,17 @@ def main(): if arguments.closed_pr is not None and arguments.closed_pr not in closed: print("The requested pull request is not confirmed closed; no caches selected.") return 0 + open_prs = set() if arguments.closed_pr is not None else { + int(match.group(1)) for cache in caches + if (match := PULL_REF.fullmatch(cache["ref"])) + and int(match.group(1)) not in closed and cache["ref"] not in unresolved + } # Check live workflow admission after gathering cache/PR metadata. A new run # can still race deletion; cache maintenance offers no atomic restore lease. running = active_refs(api, caches) plan = plan_caches(caches, default_branch=repository["default_branch"], closed_prs=closed, active_refs=running, unresolved_refs=unresolved, now=datetime.now(timezone.utc), - budget_bytes=budget_bytes, closed_pr=arguments.closed_pr) + budget_bytes=budget_bytes, closed_pr=arguments.closed_pr, open_prs=open_prs) print("Applying cache maintenance." if arguments.apply else "Dry run; pass --apply to delete these caches.") for cache in plan["deletions"]: print(json.dumps({key: cache[key] for key in ("id", "key", "ref", "size_in_bytes", "reason")})) diff --git a/.github/ci/test_cache_cleanup.py b/.github/ci/test_cache_cleanup.py index fe45e8e4..ec2c618c 100644 --- a/.github/ci/test_cache_cleanup.py +++ b/.github/ci/test_cache_cleanup.py @@ -20,6 +20,7 @@ NOW = datetime(2026, 10, 15, 12, tzinfo=timezone.utc) GIB = 1024 ** 3 COMPILER = "ccache-v1-ubuntu-24.04-X64-linux-gcc-hwloc-Release" +LAYERED = COMPILER.replace("ccache-v1-", "ccache-v2-") def cache(identifier, key=None, *, ref="refs/heads/develop", version="v1", @@ -42,6 +43,7 @@ def plan(caches, **options): return cleanup.plan_caches(caches, **{ "default_branch": "develop", "closed_prs": set(), + "open_prs": set(), "active_refs": set(), "now": NOW, **options, @@ -62,6 +64,7 @@ def test_empty(self): def test_lineages(self): for key, expected in ( (f"{COMPILER}-12345-2", COMPILER), + (f"{LAYERED}-main-12345-2", f"{LAYERED}-main"), ("dependencies-v1-Windows-X64-modules-abcd-12345-2", "dependencies-v1-Windows-X64-modules-abcd"), ("pip-v1-Windows-X64-cpython-312-abcd-12345-2", @@ -148,9 +151,85 @@ def test_baseline(self): def test_large_baseline(self): result = plan([cache(1, size=9 * GIB)]) - self.assertEqual(removed(result), {1: "default-branch baseline"}) + self.assertEqual(removed(result), {1: "develop baseline"}) self.assertEqual(result["after_bytes"], 0) + def test_both_branches(self): + rows = [cache(1, ref="refs/heads/main", size=3 * GIB, accessed=18000), + cache(2, size=3 * GIB, accessed=14400), + cache(3, key="ordinary", ref="refs/heads/feature", size=3 * GIB)] + result = plan(rows) + self.assertEqual(removed(result), {3: "least recently used"}) + self.assertEqual(result["after_bytes"], 6 * GIB) + + def test_open_priority(self): + rows = [cache(1, ref="refs/pull/3/merge", size=5 * GIB, accessed=18000), + cache(2, key="ordinary", ref="refs/heads/feature", size=4 * GIB)] + for open_prs, expected in (({3}, {2}), (set(), {1})): + with self.subTest(open_prs=open_prs): + self.assertEqual(set(removed(plan(rows, open_prs=open_prs))), expected) + + def test_open_compaction(self): + rows = [cache(1, ref="refs/pull/3/merge", age=18000), + cache(2, ref="refs/pull/3/merge", age=7200)] + result = plan(rows, open_prs={3}) + self.assertEqual(removed(result), {1: "superseded snapshot"}) + + def test_open_noncompiler(self): + rows = [cache(1, key="dependencies-v1-Windows-X64-modules-hash-1-1", + ref="refs/pull/3/merge", size=5 * GIB, accessed=18000), + cache(2, key="ordinary", ref="refs/heads/feature", size=4 * GIB)] + result = plan(rows, open_prs={3}) + self.assertEqual(removed(result), {1: "least recently used"}) + + def test_tiers(self): + rows = [cache(1, ref="refs/heads/main", size=3 * GIB, accessed=18000), + cache(2, size=3 * GIB, accessed=14400), + cache(3, ref="refs/pull/3/merge", size=3 * GIB, accessed=10800), + cache(4, key="ordinary", ref="refs/heads/feature", size=GIB)] + for budget, expected in ((9, [4]), (8, [4, 3]), (5, [4, 3, 2]), (2, [4, 3, 2, 1])): + with self.subTest(budget=budget): + result = plan(rows, open_prs={3}, budget_bytes=budget * GIB) + self.assertEqual([row["id"] for row in result["deletions"]], expected) + self.assertLessEqual(result["after_bytes"], budget * GIB) + result = plan(rows, open_prs={3}, budget_bytes=2 * GIB) + self.assertEqual(removed(result), {4: "least recently used", 3: "open pull request baseline", + 2: "develop baseline", 1: "main baseline"}) + + def test_tool_baseline(self): + for branch in ("develop", "main"): + with self.subTest(branch=branch): + rows = [cache(1, key="docs-tools-v1-Linux-X64-identity", ref=f"refs/heads/{branch}", + size=5 * GIB, accessed=18000), + cache(2, ref="refs/pull/3/merge", size=4 * GIB)] + result = plan(rows, default_branch=branch, open_prs={3}) + self.assertEqual(removed(result), {2: "open pull request baseline"}) + + def test_layered_compaction(self): + rows = [cache(1, key=f"{LAYERED}-main-1-1", ref="refs/heads/main", age=18000), + cache(2, key=f"{LAYERED}-main-2-1", ref="refs/heads/main", age=7200), + cache(3, key=f"{LAYERED}-develop-3-1")] + self.assertEqual(removed(plan(rows)), {1: "superseded snapshot"}) + + def test_migration(self): + rows = [cache(1, ref="refs/heads/main", size=5 * GIB), + cache(2, key=f"{LAYERED}-main-2-1", ref="refs/heads/main", + size=4 * GIB, accessed=18000)] + self.assertEqual(removed(plan(rows)), {1: "least recently used"}) + self.assertEqual(removed(plan(rows, budget_bytes=10 * GIB)), {}) + + def test_migration_scope(self): + rows = [cache(1, ref="refs/heads/main", size=5 * GIB), + cache(2, key=f"{LAYERED}-develop-2-1", size=4 * GIB, accessed=18000)] + self.assertEqual(removed(plan(rows)), {2: "develop baseline"}) + + def test_migration_profile(self): + other = "ccache-v2-ubuntu-24.04-X64-linux-clang-debug-Debug" + rows = [cache(1, ref="refs/heads/main", size=5 * GIB), + cache(2, key=f"{other}-main-2-1", ref="refs/heads/main", + size=4 * GIB, accessed=18000)] + self.assertEqual(removed(plan(rows)), {2: "main baseline"}) + def test_active(self): rows = [cache(1, size=5 * GIB), cache(2, size=4 * GIB)] result = plan(rows, active_refs={"refs/heads/develop"}) @@ -329,6 +408,24 @@ def test_unconfirmed_main(self): call("GET", ""), call("GET", "/pulls/3", missing_ok=True), ]) + def test_open_main(self): + api = Mock(spec=cleanup.GitHubApi) + api.request.side_effect = [{"default_branch": "develop"}, {"state": "open"}, + {"state": "closed"}, None] + rows = [cache(number, ref=f"refs/pull/{number}/merge") for number in (3, 4, 5)] + api.pages.side_effect = lambda path, field, **query: rows if path == "/actions/caches" else [] + decision = {"before_bytes": 300, "after_bytes": 300, "protected_bytes": 100, + "deletions": [], "protected_ids": [5]} + with patch.object(cleanup, "GitHubApi", return_value=api), \ + patch.object(cleanup, "plan_caches", return_value=decision) as select, \ + patch.object(cleanup.sys, "argv", ["cache_cleanup.py"]), \ + patch.dict(os.environ, {}, clear=True), redirect_stdout(io.StringIO()): + self.assertEqual(cleanup.main(), 0) + self.assertEqual(select.call_args.kwargs["open_prs"], {3}) + self.assertEqual(select.call_args.kwargs["closed_prs"], {4}) + self.assertEqual(select.call_args.kwargs["unresolved_refs"], {"refs/pull/5/merge"}) + self.assertTrue(all(invocation.args[0] == "GET" for invocation in api.request.call_args_list)) + def test_invalid_pr(self): for number in ("0", "-1", "invalid"): with self.subTest(number=number), patch.object(cleanup, "GitHubApi") as api, \ From 9a58d4d18ab324b370d65730e816383ede86905c Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 15:56:35 -0700 Subject: [PATCH 18/98] Adds layered compiler caches for main, develop and pull requests Restore separate native file-backend layers with main as baseline, develop additions and PR additions. Keep parents read-only and statistics outside archived data. Subtract inherited result keys, preserve distinct manifest mappings and nearest-parent precedence, skip unchanged uploads, and replace emptied layers with small markers. Trim only the writable layer to 1 GiB using cache-hit modification times. Share restore setup across both CI matrices and document scope rules, cold v2 migration and bounded retention. Workflow lint, Python syntax and whitespace checks passed; builds and runtime tests were not run. --- .../actions/restore-compiler-cache/action.yml | 45 +++ .github/ci/compiler_cache.py | 236 +++++++++++++ .github/ci/test_compiler_cache.py | 330 ++++++++++++++++++ .github/workflows/ci.yml | 43 +-- docs/development/building.md | 49 ++- 5 files changed, 675 insertions(+), 28 deletions(-) create mode 100644 .github/actions/restore-compiler-cache/action.yml create mode 100644 .github/ci/compiler_cache.py create mode 100644 .github/ci/test_compiler_cache.py diff --git a/.github/actions/restore-compiler-cache/action.yml b/.github/actions/restore-compiler-cache/action.yml new file mode 100644 index 00000000..8d7bbc6f --- /dev/null +++ b/.github/actions/restore-compiler-cache/action.yml @@ -0,0 +1,45 @@ +name: Restore layered compiler caches +description: Restore main, develop and current compiler layers and configure ccache. +inputs: + prefix: + description: Compiler cache format, runner, matrix profile and configuration prefix. + required: true +outputs: + current-path: + description: Writable layer to save after compilation. + value: ${{ steps.metadata.outputs.current-path }} + current-key: + description: New immutable key for the writable layer. + value: ${{ steps.metadata.outputs.current-key }} +runs: + using: composite + steps: + - name: Describe compiler cache layers + id: metadata + shell: bash + env: + CACHE_PREFIX: ${{ inputs.prefix }} + run: python .github/ci/compiler_cache.py metadata --prefix "$CACHE_PREFIX" + - name: Restore main compiler baseline + if: steps.metadata.outputs.restore-main == 'true' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.metadata.outputs.main-path }} + key: ${{ steps.metadata.outputs.main-prefix }}${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: ${{ steps.metadata.outputs.main-prefix }} + - name: Restore develop compiler additions + if: steps.metadata.outputs.restore-develop == 'true' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.metadata.outputs.develop-path }} + key: ${{ steps.metadata.outputs.develop-prefix }}${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: ${{ steps.metadata.outputs.develop-prefix }} + - name: Restore current compiler additions + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.metadata.outputs.current-path }} + key: ${{ steps.metadata.outputs.current-key }} + restore-keys: ${{ steps.metadata.outputs.current-prefix }} + - name: Configure compiler cache layers + shell: bash + run: python .github/ci/compiler_cache.py configure diff --git a/.github/ci/compiler_cache.py b/.github/ci/compiler_cache.py new file mode 100644 index 00000000..b1441dc0 --- /dev/null +++ b/.github/ci/compiler_cache.py @@ -0,0 +1,236 @@ +"""Configure ordered native ccache layers and save only current-layer additions.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path, PurePath +import re +import stat +import subprocess + + +ROOT = Path(__file__).resolve().parents[2] +PREFIX = re.compile(r"ccache-v2-[A-Za-z0-9][A-Za-z0-9_.-]*") +PULL_REF = re.compile(r"refs/pull/([1-9][0-9]*)/merge") +# Native file-backend keys and CacheEntry v1 result headers in ccache 4.9/4.14. +NATIVE_KEY = re.compile(r"[0-9a-f]{2}/[0-9a-f]{38}") +RESULT_HEADER = b"\xcc\xac\x01\x00" +EMPTY_MARKER = ".cheryl-empty-layer" +METADATA_FILES = {EMPTY_MARKER, "CACHEDIR.TAG"} + + +def _valid_ref(ref): + return (isinstance(ref, str) and ref.startswith("refs/") and not ref.endswith(("/", ".")) + and not any(char.isspace() or ord(char) < 32 or ord(char) == 127 or char in "~^:?*[\\" for char in ref) + and not any(part in ref for part in ("..", "//", "@{")) + and not any(part.startswith(".") or part.endswith(".lock") for part in ref.split("/"))) + + +def select_role(environment, event): + ref = environment.get("GITHUB_REF", "") + if not _valid_ref(ref): + raise ValueError("GITHUB_REF must be a valid full Git reference") + if environment.get("GITHUB_EVENT_NAME") == "pull_request": + match = PULL_REF.fullmatch(ref) + number = event.get("number") if isinstance(event, dict) else None + pull = event.get("pull_request") if isinstance(event, dict) else None + base_info = pull.get("base") if isinstance(pull, dict) else None + base = base_info.get("ref") if isinstance(base_info, dict) else None + if not match or type(number) is not int or number != int(match.group(1)) or not isinstance(base, str) or not _valid_ref("refs/heads/" + base): + raise ValueError("Pull request cache scope must match the event number and base branch") + return {"role": "pr", "restore-main": True, "restore-develop": base == "develop"} + if not ref.startswith(("refs/heads/", "refs/tags/")) or not ref.rsplit("/", 1)[-1]: + raise ValueError("Compiler caches require a branch, tag or pull request merge reference") + if ref == "refs/heads/main": + return {"role": "main", "restore-main": False, "restore-develop": False} + if ref == "refs/heads/develop": + return {"role": "develop", "restore-main": True, "restore-develop": False} + return {"role": "branch", "restore-main": True, "restore-develop": False} + + +def _context(environment, event): + environment = os.environ if environment is None else environment + if event is None: + event = {} + if environment.get("GITHUB_EVENT_NAME") == "pull_request": + with Path(environment["GITHUB_EVENT_PATH"]).open(encoding="utf-8") as source: + event = json.load(source) + return environment, event + + +def metadata(prefix, *, environment=None, event=None, root=ROOT): + environment, event = _context(environment, event) + if not isinstance(prefix, str) or not PREFIX.fullmatch(prefix): + raise ValueError("Compiler cache prefix must start with ccache-v2- and contain only letters, numbers, dots, underscores and hyphens") + for name in ("GITHUB_RUN_ID", "GITHUB_RUN_ATTEMPT"): + value = environment.get(name) + if not isinstance(value, str) or not re.fullmatch(r"[1-9][0-9]*", value): + raise ValueError(f"{name} must be a positive integer") + selected = select_role(environment, event) + directory = Path(root).absolute() / "build" / "cache" / "compiler" + role = selected["role"] + output = {"role": role, "main-path": str(directory / "main"), + "develop-path": str(directory / "develop"), "current-path": str(directory / role), + "main-prefix": f"{prefix}-main-", "develop-prefix": f"{prefix}-develop-", + "current-prefix": f"{prefix}-{role}-"} + output["current-key"] = output["current-prefix"] + environment["GITHUB_RUN_ID"] + "-" + environment["GITHUB_RUN_ATTEMPT"] + output.update({name: str(selected[name]).lower() for name in ("restore-main", "restore-develop")}) + return output + + +def file_url(path): + return (path if isinstance(path, PurePath) else Path(path)).as_uri() + + +def storage_configuration(current, parents): + return " ".join([file_url(current) + "|update-mtime=true", + *(file_url(parent) + "|read-only" for parent in parents)]) + + +def _files(directory): + directory = Path(directory).absolute() + for ancestor in (directory, *directory.parents): + if ancestor.is_symlink() or (hasattr(ancestor, "is_junction") and ancestor.is_junction()): + raise ValueError(f"Compiler cache paths must not contain symbolic links: {ancestor}") + if not directory.exists(): + return {} + if not directory.is_dir(): + raise ValueError(f"Compiler cache path must be a directory: {directory}") + files = {} + for parent, directories, names in os.walk(directory, followlinks=False): + for name in sorted(directories + names): + path = Path(parent) / name + mode = path.lstat().st_mode + if path.is_symlink() or (hasattr(path, "is_junction") and path.is_junction()): + raise ValueError(f"Compiler cache paths must not contain symbolic links: {path}") + if stat.S_ISREG(mode): + files[path.relative_to(directory).as_posix()] = path + elif not stat.S_ISDIR(mode): + raise ValueError(f"Compiler cache contains a non-regular file: {path}") + return files + + +def _digest(path): + digest = hashlib.sha256() + with path.open("rb") as source: + for block in iter(lambda: source.read(1024 * 1024), b""): + digest.update(block) + return digest.digest() + + +def _entries(directory): + return {name: path for name, path in _files(directory).items() if name not in METADATA_FILES} + + +def fingerprint(directory): + digest = hashlib.sha256() + for name, path in sorted(_entries(directory).items()): + encoded = name.encode("utf-8") + digest.update(len(encoded).to_bytes(8, "big")) + digest.update(encoded) + digest.update(_digest(path)) + return digest.hexdigest() + + +def _is_result(path): + with path.open("rb") as source: + return source.read(len(RESULT_HEADER)) == RESULT_HEADER + + +def deduplicate(current, parents): + current_files = _entries(current) + parent_files = [_entries(parent) for parent in parents] + removed = 0 + for name, path in current_files.items(): + parent = next((files[name] for files in parent_files if name in files), None) + if parent is None: + continue + # Result keys identify compiler inputs; serialized headers can differ by creation time. + same_result = NATIVE_KEY.fullmatch(name) and _is_result(path) and _is_result(parent) + if same_result or (path.stat().st_size == parent.stat().st_size and _digest(path) == _digest(parent)): + path.unlink() + removed += 1 + return removed + + +def _layers(environment, event, root): + info = metadata(environment.get("CHERYL_CCACHE_PREFIX"), environment=environment, event=event, root=root) + parents = [] + if info["restore-develop"] == "true": + parents.append(Path(info["develop-path"])) + if info["restore-main"] == "true": + parents.append(Path(info["main-path"])) + return info, Path(info["current-path"]), parents + + +def configure(*, environment=None, event=None, root=ROOT): + environment, event = _context(environment, event) + info, current, parents = _layers(environment, event, root) + for directory in [current, *parents]: + _files(directory) + directory.mkdir(parents=True, exist_ok=True) + stats = Path(root).absolute() / "build" / "ccache" + _files(stats) + stats.mkdir(parents=True, exist_ok=True) + state = {"key": info["current-key"], "path": str(current), "before": fingerprint(current), + "entries": len(_entries(current))} + (Path(root) / "build" / "cache" / "compiler-state.json").write_text(json.dumps(state), encoding="utf-8") + return {"CCACHE_DIR": str(stats), "CCACHE_REMOTE_ONLY": "1", + "CCACHE_REMOTE_STORAGE": storage_configuration(current, parents), + "CCACHE_NOFILECLONE": "1", "CCACHE_NOHARDLINK": "1"} + + +def prepare_save(*, environment=None, event=None, root=ROOT): + environment, event = _context(environment, event) + info, current, parents = _layers(environment, event, root) + state = json.loads((Path(root) / "build" / "cache" / "compiler-state.json").read_text(encoding="utf-8")) + if not isinstance(state, dict) or state.get("key") != info["current-key"] or state.get("path") != str(current): + raise ValueError("Compiler cache snapshot does not match this run and layer") + if not isinstance(state.get("before"), str) or not re.fullmatch(r"[a-f0-9]{64}", state["before"]) or type(state.get("entries")) is not int or state["entries"] < 0: + raise ValueError("Compiler cache snapshot does not match this run and layer") + removed = deduplicate(current, parents) + subprocess.run(["ccache", "--trim-dir", str(current), "--trim-max-size", "1GiB", + "--trim-method", "mtime"], check=True) + after = fingerprint(current) + entries = _entries(current) + marker = current / EMPTY_MARKER + if entries: + marker.unlink(missing_ok=True) + elif state["entries"]: + # Supersede a restored nonempty archive even when all entries moved to a parent. + marker.write_text("Current compiler cache layer is empty.\n", encoding="utf-8") + changed = after != state["before"] + return {"changed": str(changed).lower(), "removed": str(removed)} + + +def _emit(values, destination): + for name, value in values.items(): + if any(char in str(value) for char in "\r\n"): + raise ValueError("Compiler cache output values must be single-line") + with Path(os.environ[destination]).open("a", encoding="utf-8") as output: + for name, value in values.items(): + output.write(f"{name}={value}\n") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + describe = commands.add_parser("metadata") + describe.add_argument("--prefix", required=True) + commands.add_parser("configure") + commands.add_parser("prepare-save") + args = parser.parse_args() + if args.command == "metadata": + _emit(metadata(args.prefix), "GITHUB_OUTPUT") + _emit({"CHERYL_CCACHE_PREFIX": args.prefix}, "GITHUB_ENV") + elif args.command == "configure": + _emit(configure(), "GITHUB_ENV") + else: + result = prepare_save() + _emit(result, "GITHUB_OUTPUT") + print(f"Compiler cache additions: changed={result['changed']}, identical parent entries removed={result['removed']}") + + +if __name__ == "__main__": + main() diff --git a/.github/ci/test_compiler_cache.py b/.github/ci/test_compiler_cache.py new file mode 100644 index 00000000..42d23626 --- /dev/null +++ b/.github/ci/test_compiler_cache.py @@ -0,0 +1,330 @@ +"""Check compiler cache layers using temporary files and mocked ccache commands.""" + +import importlib.util +import os +from pathlib import Path, PurePosixPath, PureWindowsPath +import tempfile +import unittest +from unittest.mock import patch + + +spec = importlib.util.spec_from_file_location( + "compiler_cache", Path(__file__).with_name("compiler_cache.py") +) +compiler = importlib.util.module_from_spec(spec) +spec.loader.exec_module(compiler) + +PREFIX = "ccache-v2-ubuntu-24.04-X64-linux-gcc-hwloc-Release" +ENTRY_KEY = "ab/" + "c" * 38 +RESULT_HEADER = b"\xcc\xac\x01\x00" +MANIFEST_HEADER = b"\xcc\xac\x01\x01" +ENVIRONMENT = { + "GITHUB_EVENT_NAME": "push", + "GITHUB_REF": "refs/heads/main", + "GITHUB_RUN_ID": "12345", + "GITHUB_RUN_ATTEMPT": "2", + "CHERYL_CCACHE_PREFIX": PREFIX, +} + + +def pull_environment(number=3): + return {**ENVIRONMENT, "GITHUB_EVENT_NAME": "pull_request", "GITHUB_REF": f"refs/pull/{number}/merge"} + + +def pull_event(base="develop", number=3): + return {"number": number, "pull_request": {"number": number, "base": {"ref": base}}} + + +def write(directory, name, contents): + path = directory / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(contents) + return path + + +class MetadataTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name).resolve() + + def test_roles(self): + for ref, role, restore_main in ( + ("refs/heads/main", "main", False), + ("refs/heads/develop", "develop", True), + ("refs/heads/feature", "branch", True), + ("refs/tags/v4.0.0", "branch", True), + ): + with self.subTest(ref=ref): + result = compiler.select_role({**ENVIRONMENT, "GITHUB_REF": ref}, {}) + self.assertEqual(result["role"], role) + self.assertEqual(result["restore-main"], restore_main) + self.assertFalse(result["restore-develop"]) + + def test_pull_base(self): + for base, restore_develop in (("main", False), ("develop", True)): + with self.subTest(base=base): + result = compiler.select_role(pull_environment(), pull_event(base)) + self.assertEqual(result["role"], "pr") + self.assertTrue(result["restore-main"]) + self.assertEqual(result["restore-develop"], restore_develop) + + def test_pull_mismatch(self): + with self.assertRaises(ValueError): + compiler.select_role(pull_environment(3), pull_event(number=30)) + + def test_paths(self): + main = compiler.metadata(PREFIX, environment=ENVIRONMENT, event={}, root=self.root) + develop = compiler.metadata(PREFIX, environment={**ENVIRONMENT, "GITHUB_REF": "refs/heads/develop"}, + event={}, root=self.root) + pull = compiler.metadata(PREFIX, environment=pull_environment(), event=pull_event(), root=self.root) + self.assertEqual(main["current-path"], pull["main-path"]) + self.assertEqual(develop["current-path"], pull["develop-path"]) + self.assertEqual(Path(main["current-path"]), self.root / "build/cache/compiler/main") + self.assertEqual(Path(develop["current-path"]), self.root / "build/cache/compiler/develop") + self.assertEqual(Path(pull["current-path"]), self.root / "build/cache/compiler/pr") + + def test_keys(self): + result = compiler.metadata(PREFIX, environment=pull_environment(), event=pull_event(), root=self.root) + self.assertEqual(result["current-key"], f"{PREFIX}-pr-12345-2") + self.assertTrue(result["current-key"].startswith(result["current-prefix"])) + self.assertNotEqual(result["current-prefix"], result["main-prefix"]) + self.assertNotEqual(result["current-prefix"], result["develop-prefix"]) + self.assertEqual(result["restore-main"], "true") + self.assertEqual(result["restore-develop"], "true") + + def test_prefix(self): + for prefix in ("ccache-v1-profile", "ccache-v2-../escape", "ccache-v2-profile|read-only", + "ccache-v2-profile with spaces"): + with self.subTest(prefix=prefix), self.assertRaises(ValueError): + compiler.metadata(prefix, environment=ENVIRONMENT, event={}, root=self.root) + + def test_run_identity(self): + for name, value in (("GITHUB_RUN_ID", "0"), ("GITHUB_RUN_ID", "../escape"), + ("GITHUB_RUN_ATTEMPT", "0"), ("GITHUB_RUN_ATTEMPT", "retry")): + with self.subTest(name=name, value=value), self.assertRaises(ValueError): + compiler.metadata(PREFIX, environment={**ENVIRONMENT, name: value}, event={}, root=self.root) + + +class StorageTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name).resolve() + self.current = self.root / "current" + self.parent = self.root / "parent" + self.current.mkdir() + self.parent.mkdir() + + def test_file_urls(self): + self.assertEqual(compiler.file_url(PurePosixPath("/tmp/cache layer#1")), + "file:///tmp/cache%20layer%231") + self.assertEqual(compiler.file_url(PureWindowsPath(r"D:\build files\cache#1")), + "file:///D:/build%20files/cache%231") + + def test_storage_order(self): + main = self.root / "main" + result = compiler.storage_configuration(self.current, [self.parent, main]) + self.assertEqual(result, " ".join((compiler.file_url(self.current) + "|update-mtime=true", + compiler.file_url(self.parent) + "|read-only", + compiler.file_url(main) + "|read-only"))) + + def test_fingerprint(self): + path = write(self.current, "entries/result", b"compiled result") + initial = compiler.fingerprint(self.current) + os.utime(path, (1_600_000_000, 1_600_000_000)) + self.assertEqual(compiler.fingerprint(self.current), initial) + path.write_bytes(b"modified result") + os.utime(path, (1_600_000_000, 1_600_000_000)) + self.assertNotEqual(compiler.fingerprint(self.current), initial) + + def test_entry_names(self): + path = write(self.current, "entries/result", b"compiled result") + initial = compiler.fingerprint(self.current) + path.rename(path.with_name("renamed")) + self.assertNotEqual(compiler.fingerprint(self.current), initial) + + def test_deduplicate(self): + duplicate = write(self.current, "entries/result", b"compiled result") + unique = write(self.current, "entries/new", b"new result") + parent = write(self.parent, "entries/result", b"compiled result") + self.assertEqual(compiler.deduplicate(self.current, [self.parent]), 1) + self.assertFalse(duplicate.exists()) + self.assertEqual(unique.read_bytes(), b"new result") + self.assertEqual(parent.read_bytes(), b"compiled result") + + def test_changed_manifest(self): + current = write(self.current, "entries/manifest", b"new refs") + parent = write(self.parent, "entries/manifest", b"old refs") + self.assertEqual(compiler.deduplicate(self.current, [self.parent]), 0) + self.assertEqual(current.read_bytes(), b"new refs") + self.assertEqual(parent.read_bytes(), b"old refs") + + def test_result_identity(self): + current = write(self.current, ENTRY_KEY, RESULT_HEADER + b"current serialization metadata") + parent = write(self.parent, ENTRY_KEY, RESULT_HEADER + b"parent metadata") + self.assertEqual(compiler.deduplicate(self.current, [self.parent]), 1) + self.assertFalse(current.exists()) + self.assertEqual(parent.read_bytes(), RESULT_HEADER + b"parent metadata") + + def test_unknown_format(self): + for index, (name, current_bytes, parent_bytes) in enumerate(( + (ENTRY_KEY, b"\xcc\xac\x01", RESULT_HEADER + b"parent"), + (ENTRY_KEY, b"\xcc\xac\x02\x00current", b"\xcc\xac\x02\x00parent"), + (ENTRY_KEY, b"\xcc\xac\x01\x02current", b"\xcc\xac\x01\x02parent"), + (ENTRY_KEY, RESULT_HEADER + b"current", b"unknown parent"), + ("entries/result", RESULT_HEADER + b"current", RESULT_HEADER + b"parent"), + )): + with self.subTest(name=name, header=current_bytes[:4]): + current = self.current / str(index) + parent = self.parent / str(index) + entry = write(current, name, current_bytes) + write(parent, name, parent_bytes) + self.assertEqual(compiler.deduplicate(current, [parent]), 0) + self.assertEqual(entry.read_bytes(), current_bytes) + + def test_manifest_shadow(self): + main = self.root / "main" + selected = MANIFEST_HEADER + b"main refs" + entry = write(self.current, ENTRY_KEY, selected) + develop = write(self.parent, ENTRY_KEY, MANIFEST_HEADER + b"other refs") + baseline = write(main, ENTRY_KEY, selected) + self.assertEqual(compiler.deduplicate(self.current, [self.parent, main]), 0) + self.assertEqual(entry.read_bytes(), selected) + self.assertEqual(develop.read_bytes(), MANIFEST_HEADER + b"other refs") + self.assertEqual(baseline.read_bytes(), selected) + + def test_parent_miss(self): + main = self.root / "main" + entry = write(self.current, ENTRY_KEY, MANIFEST_HEADER + b"main refs") + write(main, ENTRY_KEY, MANIFEST_HEADER + b"main refs") + self.assertEqual(compiler.deduplicate(self.current, [self.parent, main]), 1) + self.assertFalse(entry.exists()) + + def test_other_path(self): + path = write(self.current, "entries/result", b"same bytes") + write(self.parent, "elsewhere/result", b"same bytes") + self.assertEqual(compiler.deduplicate(self.current, [self.parent]), 0) + self.assertTrue(path.exists()) + + def test_missing_parents(self): + path = write(self.current, "entries/result", b"own result") + self.assertEqual(compiler.deduplicate(self.current, [self.root / "missing"]), 0) + self.assertEqual(path.read_bytes(), b"own result") + + def make_link(self, path, target): + try: + path.symlink_to(target) + except (NotImplementedError, OSError) as error: + self.skipTest(f"Symbolic-link fixture unavailable on this host: {error}") + + def test_symlink(self): + target = write(self.root, "outside", b"outside layer") + self.make_link(self.current / "link", target) + with self.assertRaises(ValueError): + compiler.fingerprint(self.current) + self.assertEqual(target.read_bytes(), b"outside layer") + + def test_validate_before_delete(self): + duplicate = write(self.current, "entries/result", b"compiled result") + write(self.parent, "entries/result", b"compiled result") + outside = write(self.root, "outside", b"outside layer") + self.make_link(self.parent / "unsafe", outside) + with self.assertRaises(ValueError): + compiler.deduplicate(self.current, [self.parent]) + self.assertEqual(duplicate.read_bytes(), b"compiled result") + self.assertEqual(outside.read_bytes(), b"outside layer") + + +class WorkflowTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name).resolve() + self.environment = pull_environment() + self.event = pull_event() + self.metadata = compiler.metadata(PREFIX, environment=self.environment, event=self.event, root=self.root) + self.current = Path(self.metadata["current-path"]) + + def test_configure(self): + with patch.object(compiler.subprocess, "run") as command: + result = compiler.configure(environment=self.environment, event=self.event, root=self.root) + command.assert_not_called() + self.assertIn(result["CCACHE_REMOTE_ONLY"], ("1", "true")) + self.assertNotEqual(Path(result["CCACHE_DIR"]), self.current) + storage = result["CCACHE_REMOTE_STORAGE"] + self.assertEqual(storage, compiler.storage_configuration( + self.current, [Path(self.metadata["develop-path"]), Path(self.metadata["main-path"])])) + self.assertTrue((self.root / "build/cache/compiler-state.json").is_file()) + + def test_cold_start(self): + with patch.object(compiler.subprocess, "run") as command: + compiler.configure(environment=self.environment, event=self.event, root=self.root) + write(self.current, "entries/result", b"new result") + result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) + self.assertEqual(result["changed"], "true") + self.assertEqual(result["removed"], "0") + self.assertEqual(command.call_count, 1) + self.assertEqual(command.call_args.args[0], ["ccache", "--trim-dir", str(self.current), + "--trim-max-size", "1GiB", "--trim-method", "mtime"]) + self.assertTrue(command.call_args.kwargs["check"]) + + def test_hit_only(self): + path = write(self.current, "entries/result", b"own result") + with patch.object(compiler.subprocess, "run"): + compiler.configure(environment=self.environment, event=self.event, root=self.root) + os.utime(path, (1_600_000_000, 1_600_000_000)) + result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) + self.assertEqual(result["changed"], "false") + + def test_parent_subtraction(self): + parent = Path(self.metadata["develop-path"]) + parent_file = write(parent, "entries/result", b"shared result") + with patch.object(compiler.subprocess, "run"): + compiler.configure(environment=self.environment, event=self.event, root=self.root) + duplicate = write(self.current, "entries/result", b"shared result") + write(self.current, "entries/new", b"own result") + result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) + self.assertEqual(result, {"changed": "true", "removed": "1"}) + self.assertFalse(duplicate.exists()) + self.assertEqual(parent_file.read_bytes(), b"shared result") + + def test_empty_layer(self): + with patch.object(compiler.subprocess, "run"): + compiler.configure(environment=self.environment, event=self.event, root=self.root) + result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) + self.assertEqual(result["changed"], "false") + + def test_empty_replacement(self): + write(Path(self.metadata["develop-path"]), "entries/result", b"shared result") + write(self.current, "entries/result", b"shared result") + empty = self.root / "empty" + empty.mkdir() + with patch.object(compiler.subprocess, "run"): + compiler.configure(environment=self.environment, event=self.event, root=self.root) + result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) + self.assertEqual(result, {"changed": "true", "removed": "1"}) + self.assertTrue((self.current / ".cheryl-empty-layer").is_file()) + self.assertEqual(compiler.fingerprint(self.current), compiler.fingerprint(empty)) + + def test_restored_empty(self): + write(self.current, ".cheryl-empty-layer", b"") + with patch.object(compiler.subprocess, "run"): + compiler.configure(environment=self.environment, event=self.event, root=self.root) + result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) + self.assertEqual(result["changed"], "false") + self.assertEqual(result["removed"], "0") + + def test_new_entries(self): + marker = write(self.current, ".cheryl-empty-layer", b"") + with patch.object(compiler.subprocess, "run"): + compiler.configure(environment=self.environment, event=self.event, root=self.root) + entry = write(self.current, "entries/result", b"own result") + result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) + self.assertEqual(result["changed"], "true") + self.assertFalse(marker.exists()) + self.assertEqual(entry.read_bytes(), b"own result") + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60a73e72..663ab17e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,6 @@ permissions: env: CMAKE_C_COMPILER_LAUNCHER: ccache CMAKE_CXX_COMPILER_LAUNCHER: ccache - CCACHE_MAXSIZE: 1G CCACHE_COMPILERCHECK: content concurrency: @@ -25,8 +24,6 @@ jobs: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} timeout-minutes: 60 - env: - CCACHE_DIR: ${{ github.workspace }}/build/ccache strategy: fail-fast: false matrix: @@ -46,19 +43,18 @@ jobs: profile: headless - name: Restore compiler cache id: ccache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: ./.github/actions/restore-compiler-cache with: - path: ${{ env.CCACHE_DIR }} - key: ccache-v1-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.name }}-${{ matrix.type }}-${{ github.run_id }}-${{ github.run_attempt }} - restore-keys: | - ccache-v1-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.name }}-${{ matrix.type }}- + prefix: ccache-v2-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.name }}-${{ matrix.type }} - name: Reset compiler cache statistics id: ccache-ready run: ccache --zero-stats - name: Check CI tools + shell: bash run: | python .github/ci/test_ci.py python .github/ci/test_cache_cleanup.py + python .github/ci/test_compiler_cache.py - name: Build and test Linux if: runner.os == 'Linux' env: @@ -94,12 +90,17 @@ jobs: - name: Report compiler cache statistics if: ${{ !cancelled() && steps.ccache-ready.outcome == 'success' }} run: ccache --show-stats --verbose + - name: Prepare compiler cache additions + if: ${{ !cancelled() && steps.ccache-ready.outcome == 'success' }} + id: compiler-cache-state + shell: bash + run: python .github/ci/compiler_cache.py prepare-save - name: Save compiler cache - if: ${{ !cancelled() && steps.ccache-ready.outcome == 'success' && steps.ccache.outputs.cache-hit != 'true' }} + if: ${{ !cancelled() && steps.compiler-cache-state.outputs.changed == 'true' }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - path: ${{ env.CCACHE_DIR }} - key: ${{ steps.ccache.outputs.cache-primary-key }} + path: ${{ steps.ccache.outputs.current-path }} + key: ${{ steps.ccache.outputs.current-key }} - name: Retain results if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 @@ -116,8 +117,6 @@ jobs: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} timeout-minutes: 90 - env: - CCACHE_DIR: ${{ github.workspace }}/build/ccache strategy: fail-fast: false matrix: @@ -135,12 +134,9 @@ jobs: profile: modules - name: Restore compiler cache id: ccache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: ./.github/actions/restore-compiler-cache with: - path: ${{ env.CCACHE_DIR }} - key: ccache-v1-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.name }}-${{ matrix.type }}-${{ github.run_id }}-${{ github.run_attempt }} - restore-keys: | - ccache-v1-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.name }}-${{ matrix.type }}- + prefix: ccache-v2-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.name }}-${{ matrix.type }} - name: Reset compiler cache statistics id: ccache-ready run: ccache --zero-stats @@ -181,12 +177,17 @@ jobs: - name: Report compiler cache statistics if: ${{ !cancelled() && steps.ccache-ready.outcome == 'success' }} run: ccache --show-stats --verbose + - name: Prepare compiler cache additions + if: ${{ !cancelled() && steps.ccache-ready.outcome == 'success' }} + id: compiler-cache-state + shell: bash + run: python .github/ci/compiler_cache.py prepare-save - name: Save compiler cache - if: ${{ !cancelled() && steps.ccache-ready.outcome == 'success' && steps.ccache.outputs.cache-hit != 'true' }} + if: ${{ !cancelled() && steps.compiler-cache-state.outputs.changed == 'true' }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - path: ${{ env.CCACHE_DIR }} - key: ${{ steps.ccache.outputs.cache-primary-key }} + path: ${{ steps.ccache.outputs.current-path }} + key: ${{ steps.ccache.outputs.current-key }} - name: Retain checked SDK candidate if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 diff --git a/docs/development/building.md b/docs/development/building.md index 3bca1906..2deb96f7 100644 --- a/docs/development/building.md +++ b/docs/development/building.md @@ -368,19 +368,54 @@ intentionally skipped on PR, `develop` and manual runs. ### Actions cache retention Compiler and dependency caches speed up subsequent CI runs. They are separate -from the SDK and diagnostic artifacts above. `CCACHE_MAXSIZE=1G` limits each -job's compiler cache, rather than the repository's total Actions cache storage. -Compiler, Windows dependency and pip caches use new run-specific keys so their -contents can grow between builds: GitHub caches are immutable after saving. +from the SDK and diagnostic artifacts above. Each compiler matrix configuration +has separate ccache layers: + +| Build | Readable layers | Writable layer saved after building | +| --- | --- | --- | +| `main` | Main baseline A | A | +| `develop` | A + develop additions B | B | +| PR targeting `develop` | A + B + PR additions C | C | +| PR targeting `main` | A + C | C | + +[restore-compiler-cache](../../.github/actions/restore-compiler-cache/action.yml) +restores the available layers into separate directories. +[compiler_cache.py](../../.github/ci/compiler_cache.py) configures ccache to check +the current layer first, followed by its parents, and write compilation results +only into the current layer. Parent hits stay in the parent layers. Statistics +live outside the archived data. Before saving, the helper removes compiled +results already available under the same cache key in a parent. It preserves +distinct lookup metadata, then trims the writable layer to 1 GiB. +An unchanged layer is not uploaded again. A layer that becomes empty replaces its +previous snapshot with a small empty-layer marker, allowing cleanup to remove +the older duplicated data. + +This hierarchy requires `main` to be GitHub's default branch. A PR can read its +own cache, its base branch's cache and the default branch's cache; a PR targeting +`main` cannot read `develop`'s cache. Manual feature-branch builds use their own +writable layer and the main baseline. These are +[GitHub cache scope restrictions](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache). +Missing parents cause normal cache misses until their builds populate them. +The `ccache-v2` keys distinguish this file-backend layout from the previous +ordinary local-cache archives; the first build of each configuration seeds the +new format. Windows dependency and pip caches continue using full snapshots. +All updated snapshots use new run-specific keys because saved GitHub caches are +immutable. [cache-maintenance.yml](../../.github/workflows/cache-maintenance.yml) runs after the build or documentation workflow finishes and daily at 04:23 UTC. It removes caches belonging to closed pull requests, keeps the newest compiler/dependency/pip snapshot per branch, configuration and cache version, and trims older caches toward an 8 GiB repository budget. Active workflow refs and caches created or -accessed within the last 15 minutes are protected. The newest known default-branch -snapshots are evicted last if the budget requires it. The budget is a target: -concurrent uploads and protected caches can temporarily keep usage above it. +accessed within the last 15 minutes are protected. After removing redundant +snapshots, budget eviction removes ordinary old caches first, then the latest +open-PR compiler layers, then develop layers, and finally main layers. Known +default-branch tooling caches have the same priority as develop layers. Each +tier uses oldest access first. Old-format compiler snapshots lose their branch +priority once the same branch and configuration has a new-format replacement. +These priorities preserve useful branch caches where space allows; the budget +can still require evicting a current layer. The budget is a target: concurrent +uploads and protected caches can temporarily keep usage above it. Cleanup deletes cached build inputs, without deleting SDK artifacts or changing build results; an evicted cache is regenerated when next needed. From c67a9e5196095bad4701de4c691b29fe7484d646 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 16:00:58 -0700 Subject: [PATCH 19/98] Separates branch dependency caches from the shared Actions environment Keep build-deps-v2 source Git objects and vcpkg binary archives in main/develop scopes. PRs consume their base branch cache without publishing dependency snapshots; source pins update source identities and package/runner inputs update vcpkg identities independently. Define actions-env-cache-v2 in a central environment manifest and share exact immutable tool, pip, apt and documentation environments from trusted main runs. Hash environment inputs, runner images and component specifications without branch, run or submodule identity. Extend bounded cleanup for the new families and add isolated identity/publish-policy fixtures. Workflow lint, Python syntax, JSON and whitespace checks passed. Builds, package installations and runtime tests were not run. --- .../actions/prepare-dependencies/action.yml | 18 +- .github/ci/cache_cleanup.py | 16 +- .github/ci/dependencies.py | 122 +++++-- .github/ci/environment.json | 35 ++ .github/ci/test_cache_cleanup.py | 51 +++ .github/ci/test_dependencies.py | 333 ++++++++++++++++++ .github/workflows/ci.yml | 9 +- .github/workflows/documentation.yml | 14 +- 8 files changed, 553 insertions(+), 45 deletions(-) create mode 100644 .github/ci/environment.json create mode 100644 .github/ci/test_dependencies.py diff --git a/.github/actions/prepare-dependencies/action.yml b/.github/actions/prepare-dependencies/action.yml index ae3c0310..c46ce056 100644 --- a/.github/actions/prepare-dependencies/action.yml +++ b/.github/actions/prepare-dependencies/action.yml @@ -4,6 +4,10 @@ inputs: profile: description: CI composition, headless or modules. required: true +outputs: + dependency-cache-writer: + description: Whether this run may publish its main or develop build dependency cache. + value: ${{ steps.metadata.outputs.dependency-cache-writer }} runs: using: composite steps: @@ -19,13 +23,16 @@ runs: with: path: ${{ steps.metadata.outputs.submodule-path }} key: ${{ steps.metadata.outputs.submodule-key }} + restore-keys: | + ${{ steps.metadata.outputs.submodule-prefix }} + ${{ steps.metadata.outputs.submodule-main-prefix }} - name: Check out pinned submodules shell: bash env: CHERYL_SUBMODULE_TOKEN: ${{ github.token }} run: python .github/ci/dependencies.py submodules - name: Save submodule Git objects - if: steps.submodules.outputs.cache-hit != 'true' + if: steps.metadata.outputs.dependency-cache-writer == 'true' && steps.submodules.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.metadata.outputs.submodule-path }} @@ -58,7 +65,7 @@ runs: shell: bash run: python .github/ci/dependencies.py windows-tools - name: Save Windows tools - if: runner.os == 'Windows' && steps.tools.outputs.cache-hit != 'true' + if: runner.os == 'Windows' && steps.metadata.outputs.environment-cache-writer == 'true' && steps.tools.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | @@ -72,9 +79,6 @@ runs: with: path: ${{ steps.metadata.outputs.pip-path }} key: ${{ steps.metadata.outputs.pip-key }} - restore-keys: | - ${{ steps.metadata.outputs.pip-exact-prefix }} - ${{ steps.metadata.outputs.pip-prefix }} - name: Snapshot Python cache if: runner.os == 'Windows' && inputs.profile == 'modules' shell: bash @@ -89,7 +93,7 @@ runs: shell: bash run: python .github/ci/dependencies.py cache-state --cache pip - name: Save Python downloads and wheels - if: runner.os == 'Windows' && inputs.profile == 'modules' && steps.pip-state.outputs.changed == 'true' + if: runner.os == 'Windows' && inputs.profile == 'modules' && steps.metadata.outputs.environment-cache-writer == 'true' && steps.pip.outputs.cache-hit != 'true' && steps.pip-state.outputs.changed == 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.metadata.outputs.pip-path }} @@ -119,7 +123,7 @@ runs: DEPENDENCY_PROFILE: ${{ inputs.profile }} run: python .github/ci/dependencies.py apt-install --profile "$DEPENDENCY_PROFILE" - name: Save Linux package binaries - if: runner.os == 'Linux' && steps.apt-plan.outputs.needed == 'true' && steps.apt.outputs.cache-hit != 'true' + if: runner.os == 'Linux' && steps.metadata.outputs.environment-cache-writer == 'true' && steps.apt-plan.outputs.needed == 'true' && steps.apt.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.metadata.outputs.dependency-path }}/apt/*.deb diff --git a/.github/ci/cache_cleanup.py b/.github/ci/cache_cleanup.py index 654cf28a..a9faec3b 100644 --- a/.github/ci/cache_cleanup.py +++ b/.github/ci/cache_cleanup.py @@ -14,10 +14,13 @@ GIB = 1024 ** 3 -SNAPSHOT = re.compile(r"^((?:ccache-v[12]|dependencies-v1|pip-v1)-.+)-[0-9]+-[0-9]+$") +SNAPSHOT = re.compile( + r"^((?:ccache-v[12]|dependencies-v1|pip-v1)-.+|build-deps-v2-.+-binaries-[0-9a-f]+)-[0-9]+-[0-9]+$" +) PULL_REF = re.compile(r"^refs/pull/([1-9][0-9]*)/merge$") KNOWN_PREFIXES = ("ccache-v1-", "ccache-v2-", "dependencies-v1-", "pip-v1-", "docs-tools-v1-", - "windows-tools-v1-", "submodule-git-v1-", "apt-debs-v1-") + "windows-tools-v1-", "submodule-git-v1-", "apt-debs-v1-", "build-deps-v2-") +ENVIRONMENT_KEY = re.compile(r"actions-env-cache-v[1-9][0-9]*-.+") MAINTENANCE_PATH = ".github/workflows/cache-maintenance.yml" @@ -42,8 +45,8 @@ def plan_caches(caches, *, default_branch, closed_prs, active_refs, now, Active/unresolved refs and recently created or accessed entries are protected. Exact-PR mode never compacts or trims entries outside that confirmed closed PR. - Newest compiler snapshots prefer main, develop, then confirmed open PRs when - the budget requires eviction; these priorities do not reserve storage. + Newest compiler and build-dependency snapshots prefer main, develop, then + confirmed open PRs when the budget requires eviction. Priorities reserve no storage. """ if not isinstance(default_branch, str) or not default_branch or now.tzinfo is None: raise ValueError("A default branch and timezone-aware current time are required") @@ -109,9 +112,10 @@ def remove(cache, reason): priority, reason = 0, "least recently used" replaced = cache["key"].startswith("ccache-v1-") and (cache["ref"], logical) in layered_profiles if not replaced: - if cache["ref"] == "refs/heads/" + default_branch and cache["key"].startswith(KNOWN_PREFIXES): + known = cache["key"].startswith(KNOWN_PREFIXES) or ENVIRONMENT_KEY.fullmatch(cache["key"]) + if cache["ref"] == "refs/heads/" + default_branch and known: priority, reason = 2, "default-branch baseline" - if logical is not None and cache["key"].startswith(("ccache-v1-", "ccache-v2-")): + if logical is not None and cache["key"].startswith(("ccache-v1-", "ccache-v2-", "build-deps-v2-")): match = PULL_REF.fullmatch(cache["ref"]) if cache["ref"] == "refs/heads/main": priority, reason = 3, "main baseline" diff --git a/.github/ci/dependencies.py b/.github/ci/dependencies.py index a8c65764..772b2f67 100644 --- a/.github/ci/dependencies.py +++ b/.github/ci/dependencies.py @@ -12,15 +12,75 @@ ROOT = Path(__file__).resolve().parents[2] -CCACHE_VERSION = "4.14" -CCACHE_PACKAGE_VERSION = "4.14.0" -APT_COMMON = ["ccache", "ninja-build", "pkg-config", "g++-14", "libfreetype-dev", "libharfbuzz-dev", - "libicu-dev", "libhwloc-dev", "libdw-dev", "libelf-dev", "fonts-urw-base35"] -APT_EXTRA = { - "headless": ["clang-18", "clang-tools-18"], - "modules": ["libgl-dev", "libx11-dev", "libxrandr-dev", "libxinerama-dev", "libxcursor-dev", - "libxi-dev", "libxext-dev", "python3-jinja2"], -} + + +def dependency_scope(*, environment=None): + environment = os.environ if environment is None else environment + ref = environment.get("GITHUB_REF") + event = environment.get("GITHUB_EVENT_NAME") + branch = environment.get("GITHUB_BASE_REF") if event == "pull_request" else { + "refs/heads/main": "main", "refs/heads/develop": "develop" + }.get(ref) + writer = (environment.get("GITHUB_REPOSITORY") == "cppcooper/cheryl-engine" + and ref in {"refs/heads/main", "refs/heads/develop"} + and event in {"push", "workflow_dispatch"}) + return {"dependency-role": "develop" if branch == "develop" else "main", + "dependency-cache-writer": str(writer).lower()} + + +def environment_configuration(): + with (ROOT / ".github/ci/environment.json").open(encoding="utf-8") as source: + return json.load(source) + + +def environment_metadata(profile, *, environment=None, configuration=None): + """Describe immutable environment caches without inspecting Git or source revisions.""" + environment = os.environ if environment is None else environment + configuration = environment_configuration() if configuration is None else configuration + if profile not in {"headless", "modules", "docs"}: + raise ValueError("Unknown environment cache profile") + revision = configuration.get("cache_version") if isinstance(configuration, dict) else None + if type(revision) is not int or revision <= 0: + raise ValueError("The environment cache version must be a positive integer") + runner = [environment.get(name) for name in ("RUNNER_OS", "RUNNER_ARCH")] + if any(not isinstance(value, str) or not value or any( + not (character.isascii() and (character.isalnum() or character in "_.-")) + for character in value) for value in runner): + raise ValueError("Environment cache keys require a runner OS and architecture") + image = {name: environment.get(name, "") for name in ("ImageOS", "ImageVersion")} + if any(not isinstance(value, str) for value in image.values()): + raise ValueError("Runner image identities must be strings") + identity = digest(json.dumps({"configuration": configuration, "image": image}, + sort_keys=True, separators=(",", ":")).encode()) + writer = (environment.get("GITHUB_REPOSITORY") == "cppcooper/cheryl-engine" + and environment.get("GITHUB_REF") == "refs/heads/main" + and environment.get("GITHUB_EVENT_NAME") in {"push", "workflow_dispatch"}) + return {"environment-prefix": f"actions-env-cache-v{revision}-{'-'.join(runner)}-{profile}-{identity}", + "environment-cache-writer": str(writer).lower()} + + +def environment_keys(profile, baseline, requirements, *, python_tag, environment=None, configuration=None): + if not isinstance(baseline, str) or not baseline: + raise ValueError("The vcpkg tool baseline must be nonempty") + if not isinstance(requirements, bytes) or not isinstance(python_tag, str) or not python_tag or any( + not (character.isascii() and (character.isalnum() or character in "_.-")) for character in python_tag): + raise ValueError("Python cache keys require requirement bytes and an interpreter ABI tag") + values = environment_metadata(profile, environment=environment, configuration=configuration) + prefix = values["environment-prefix"] + values.update({"tools-key": f"{prefix}-tools-{digest(baseline.encode())}", + "pip-key": f"{prefix}-pip-{python_tag}-{digest(requirements)}"}) + return values + + +def apt_cache_key(profile, uris, *, environment=None, configuration=None): + if isinstance(uris, str): + raise ValueError("APT cache keys require a sequence of resolved package URI lines") + uris = tuple(uris) + if any(not isinstance(value, str) for value in uris): + raise ValueError("APT cache keys require a sequence of resolved package URI lines") + prefix = environment_metadata(profile, environment=environment, configuration=configuration)["environment-prefix"] + downloads = digest("\n".join(sorted(uris)).encode()) + return f"{prefix}-apt-{downloads}" def run(command, *, capture=False, cwd=ROOT, environment=None): @@ -45,6 +105,8 @@ def digest(value): def metadata(profile): + if profile not in {"headless", "modules"}: + raise ValueError("Dependency preparation requires the headless or modules profile") runner = f"{os.environ['RUNNER_OS']}-{os.environ['RUNNER_ARCH']}" manifest = (ROOT / ".github/ci/vcpkg.json").read_bytes() baseline = json.loads(manifest)["builtin-baseline"] @@ -58,27 +120,30 @@ def metadata(profile): cache = ROOT / "build/cache/dependencies" vcpkg = Path(os.environ["RUNNER_TEMP"]).resolve() / "vcpkg" ccache = ROOT / "build/cache/tools/ccache" - dependency_prefix = f"dependencies-v1-{runner}-{profile}-" - dependency_tag = digest(manifest + b"\0" + requirements) + scope = dependency_scope() + role = scope["dependency-role"] + source_prefix = f"build-deps-v2-{runner}-{role}-sources-" + dependency_prefix = f"build-deps-v2-{runner}-{role}-{profile}-binaries-" + image = {name: os.environ.get(name, "") for name in ("ImageOS", "ImageVersion")} + dependency_tag = digest(manifest + b"\0" + json.dumps(image, sort_keys=True).encode()) run_id = f"{os.environ['GITHUB_RUN_ID']}-{os.environ['GITHUB_RUN_ATTEMPT']}" dependency_key = f"{dependency_prefix}{dependency_tag}-{run_id}" - pip_prefix = f"pip-v1-{runner}-{sys.implementation.cache_tag}-" pip = ROOT / "build/cache/pip" emit("GITHUB_OUTPUT", { - "submodule-key": f"submodule-git-v1-{runner}-{submodules}", + **environment_keys(profile, baseline, requirements, python_tag=sys.implementation.cache_tag), + **scope, + "submodule-key": source_prefix + submodules, + "submodule-prefix": source_prefix, + "submodule-main-prefix": f"build-deps-v2-{runner}-main-sources-", "submodule-path": modules_path.resolve().as_posix(), - "tools-key": f"windows-tools-v1-{runner}-{baseline}-ccache-{CCACHE_PACKAGE_VERSION}", "vcpkg-root": vcpkg.as_posix(), "ccache-tools": ccache.as_posix(), "dependency-path": cache.as_posix(), "dependency-key": dependency_key, "dependency-exact-prefix": f"{dependency_prefix}{dependency_tag}-", "dependency-profile-prefix": dependency_prefix, - "dependency-runner-prefix": f"dependencies-v1-{runner}-", + "dependency-runner-prefix": f"build-deps-v2-{runner}-main-{profile}-binaries-", "pip-path": pip.as_posix(), - "pip-key": f"{pip_prefix}{digest(requirements)}-{run_id}", - "pip-exact-prefix": f"{pip_prefix}{digest(requirements)}-", - "pip-prefix": pip_prefix, }) environment = { "CHERYL_CI_DEPENDENCY_CACHE_DIR": cache.as_posix(), @@ -120,6 +185,7 @@ def submodules(): def windows_tools(): + versions = environment_configuration()["windows"] baseline = json.loads((ROOT / ".github/ci/vcpkg.json").read_text(encoding="utf-8"))["builtin-baseline"] vcpkg = Path(os.environ["CHERYL_CI_VCPKG_ROOT"]) if not (vcpkg / ".git").exists(): @@ -133,14 +199,14 @@ def windows_tools(): tools = Path(os.environ["CHERYL_CI_CCACHE_TOOLS"]) executable = tools / "ccache.exe" if not executable.is_file(): - run(["choco", "install", "ccache", f"--version={CCACHE_PACKAGE_VERSION}", "--yes", "--no-progress"]) + run(["choco", "install", "ccache", f"--version={versions['ccache_package_version']}", "--yes", "--no-progress"]) installed = Path(os.environ["ChocolateyInstall"]) / "lib/ccache/tools" candidates = sorted(installed.rglob("ccache.exe")) if not candidates: raise RuntimeError("ccache executable is unavailable") shutil.copytree(candidates[0].parent, tools, dirs_exist_ok=True) version = run([str(executable), "--version"], capture=True).splitlines()[0] - if version != f"ccache version {CCACHE_VERSION}": + if version != f"ccache version {versions['ccache_version']}": raise RuntimeError(f"Unexpected ccache version: {version}") print(version, flush=True) with Path(os.environ["GITHUB_PATH"]).open("a", encoding="utf-8") as output: @@ -148,10 +214,11 @@ def windows_tools(): def apt_command(profile): + packages = environment_configuration()["linux"] cache = Path(os.environ["CHERYL_CI_DEPENDENCY_CACHE_DIR"]) / "apt" return ["sudo", "apt-get", "-o", f"Dir::Cache::archives={cache}", "-o", "APT::Keep-Downloaded-Packages=true", "--yes", "install", - *APT_COMMON, *APT_EXTRA[profile]] + *packages["apt_common"], *packages["apt_extra"][profile]] def apt_plan(profile): @@ -161,10 +228,9 @@ def apt_plan(profile): command.insert(2, "--print-uris") plan = run(command, capture=True, environment=environment) uris = sorted(line for line in plan.splitlines() if line.startswith("'")) - downloads = digest("\n".join(uris).encode()) emit("GITHUB_OUTPUT", { "needed": str(bool(uris)).lower(), - "key": f"apt-debs-v1-{os.environ['RUNNER_OS']}-{os.environ['RUNNER_ARCH']}-{downloads}", + "key": apt_cache_key(profile, uris), }) @@ -187,12 +253,16 @@ def cache_state(name, snapshot): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("operation", choices=("metadata", "submodules", "windows-tools", "apt-plan", "apt-install", "cache-state")) - parser.add_argument("--profile", choices=("headless", "modules"), default="headless") + parser.add_argument("operation", choices=("metadata", "environment-prefix", "submodules", "windows-tools", "apt-plan", "apt-install", "cache-state")) + parser.add_argument("--profile", choices=("headless", "modules", "docs"), default="headless") parser.add_argument("--cache", choices=("vcpkg", "pip"), default="vcpkg") parser.add_argument("--snapshot", action="store_true") args = parser.parse_args() - if args.operation == "metadata": + if args.profile == "docs" and args.operation != "environment-prefix": + parser.error("The docs profile only describes environment cache prefixes") + if args.operation == "environment-prefix": + emit("GITHUB_OUTPUT", environment_metadata(args.profile)) + elif args.operation == "metadata": metadata(args.profile) elif args.operation == "submodules": submodules() diff --git a/.github/ci/environment.json b/.github/ci/environment.json new file mode 100644 index 00000000..02b1d0b8 --- /dev/null +++ b/.github/ci/environment.json @@ -0,0 +1,35 @@ +{ + "cache_version": 2, + "windows": { + "ccache_version": "4.14", + "ccache_package_version": "4.14.0" + }, + "linux": { + "apt_common": [ + "ccache", + "ninja-build", + "pkg-config", + "g++-14", + "libfreetype-dev", + "libharfbuzz-dev", + "libicu-dev", + "libhwloc-dev", + "libdw-dev", + "libelf-dev", + "fonts-urw-base35" + ], + "apt_extra": { + "headless": ["clang-18", "clang-tools-18"], + "modules": [ + "libgl-dev", + "libx11-dev", + "libxrandr-dev", + "libxinerama-dev", + "libxcursor-dev", + "libxi-dev", + "libxext-dev", + "python3-jinja2" + ] + } + } +} diff --git a/.github/ci/test_cache_cleanup.py b/.github/ci/test_cache_cleanup.py index ec2c618c..4821381d 100644 --- a/.github/ci/test_cache_cleanup.py +++ b/.github/ci/test_cache_cleanup.py @@ -21,6 +21,7 @@ GIB = 1024 ** 3 COMPILER = "ccache-v1-ubuntu-24.04-X64-linux-gcc-hwloc-Release" LAYERED = COMPILER.replace("ccache-v1-", "ccache-v2-") +BUILD_DEPS = "build-deps-v2-Windows-X64-main-modules-binaries-" + "a" * 64 def cache(identifier, key=None, *, ref="refs/heads/develop", version="v1", @@ -69,6 +70,7 @@ def test_lineages(self): "dependencies-v1-Windows-X64-modules-abcd"), ("pip-v1-Windows-X64-cpython-312-abcd-12345-2", "pip-v1-Windows-X64-cpython-312-abcd"), + (f"{BUILD_DEPS}-12345-2", BUILD_DEPS), ): with self.subTest(key=key): self.assertEqual(cleanup.lineage(key), expected) @@ -79,6 +81,8 @@ def test_lineages(self): "docs-tools-v1-Linux-X64-12345-2", "unrelated-12345-2", f"{COMPILER}-12345-retry", + "build-deps-v2-Linux-X64-main-sources-12345-2", + "actions-env-cache-v2-Linux-X64-modules-identity-tools-12345-2", ): with self.subTest(key=key): self.assertIsNone(cleanup.lineage(key)) @@ -108,6 +112,44 @@ def test_immutable(self): cache(2, key="apt-debs-v1-Linux-X64-456-1")] self.assertEqual(removed(plan(rows)), {}) + def test_build_sources(self): + prefix = "build-deps-v2-Linux-X64-main-sources-" + rows = [cache(1, key=prefix + "a" * 64, ref="refs/heads/main"), + cache(2, key=prefix + "b" * 64, ref="refs/heads/main")] + self.assertEqual(removed(plan(rows)), {}) + + def test_build_compaction(self): + rows = [cache(1, key=f"{BUILD_DEPS}-1-1", ref="refs/heads/main", age=18000), + cache(2, key=f"{BUILD_DEPS}-2-1", ref="refs/heads/main", age=7200)] + self.assertEqual(removed(plan(rows)), {1: "superseded snapshot"}) + + def test_build_isolation(self): + rows = [cache(1, key=f"{BUILD_DEPS}-1-1", ref="refs/heads/main"), + cache(2, key=f"{BUILD_DEPS}-2-1", ref="refs/heads/develop"), + cache(3, key=f"{BUILD_DEPS}-3-1", ref="refs/heads/main", version="other-path"), + cache(4, key=BUILD_DEPS.replace("-modules-", "-headless-") + "-4-1", ref="refs/heads/main"), + cache(5, key=BUILD_DEPS.replace("a" * 64, "b" * 64) + "-5-1", ref="refs/heads/main"), + cache(6, key=f"{LAYERED}-main-6-1", ref="refs/heads/main")] + self.assertEqual(removed(plan(rows)), {}) + + def test_build_priorities(self): + rows = [cache(1, key=f"{BUILD_DEPS}-1-1", ref="refs/heads/main", size=5 * GIB, accessed=18000), + cache(2, key=BUILD_DEPS.replace("-main-", "-develop-") + "-2-1", size=4 * GIB)] + self.assertEqual(removed(plan(rows, default_branch="main")), {2: "develop baseline"}) + rows[1]["ref"] = "refs/pull/3/merge" + for open_prs, expected in (({3}, "open pull request baseline"), (set(), "least recently used")): + with self.subTest(open_prs=open_prs): + self.assertEqual(removed(plan(rows, default_branch="main", open_prs=open_prs)), {2: expected}) + + def test_environment_versions(self): + for revision in (2, 3): + with self.subTest(revision=revision): + rows = [cache(1, key=f"actions-env-cache-v{revision}-Linux-X64-modules-identity-tools", + ref="refs/heads/main", size=5 * GIB, accessed=18000), + cache(2, key="ordinary", ref="refs/heads/feature", size=4 * GIB)] + result = plan(rows, default_branch="main") + self.assertEqual(removed(result), {2: "least recently used"}) + def test_closed(self): rows = [cache(1, ref="refs/pull/3/merge"), cache(2, ref="refs/pull/30/merge"), @@ -124,6 +166,15 @@ def test_closed_only(self): result = plan(rows, closed_prs={3, 4}, closed_pr=3) self.assertEqual(removed(result), {1: "closed pull request"}) + def test_closed_families(self): + keys = [f"{COMPILER}-1-1", f"{LAYERED}-pr-2-1", "dependencies-v1-Windows-X64-modules-hash-3-1", + "pip-v1-Windows-X64-cpython-313-hash-4-1", f"{BUILD_DEPS}-5-1", + "build-deps-v2-Linux-X64-main-sources-hash", "actions-env-cache-v2-Linux-X64-docs-identity-tools"] + rows = [cache(index, key=key, ref="refs/pull/3/merge") for index, key in enumerate(keys, 1)] + rows.append(cache(8, ref="refs/pull/30/merge")) + result = plan(rows, closed_prs={3}, closed_pr=3) + self.assertEqual(removed(result), {index: "closed pull request" for index in range(1, 8)}) + def test_unconfirmed(self): result = plan([cache(1, ref="refs/pull/3/merge")], closed_pr=3) self.assertEqual(removed(result), {}) diff --git a/.github/ci/test_dependencies.py b/.github/ci/test_dependencies.py new file mode 100644 index 00000000..d4f81a2b --- /dev/null +++ b/.github/ci/test_dependencies.py @@ -0,0 +1,333 @@ +"""Check dependency and shared environment cache rules without installing packages.""" + +from copy import deepcopy +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + + +spec = importlib.util.spec_from_file_location("ci_dependencies", Path(__file__).with_name("dependencies.py")) +dependencies = importlib.util.module_from_spec(spec) +spec.loader.exec_module(dependencies) + +CONFIGURATION = { + "cache_version": 2, + "windows": {"ccache_version": "4.14", "ccache_package_version": "4.14.0"}, + "linux": { + "apt_common": ["ccache", "ninja-build"], + "apt_extra": {"headless": ["clang-18"], "modules": ["libgl-dev"]}, + }, +} +ENVIRONMENT = { + "RUNNER_OS": "Linux", "RUNNER_ARCH": "X64", "ImageOS": "ubuntu24", + "ImageVersion": "20261012.1.0", "GITHUB_REPOSITORY": "cppcooper/cheryl-engine", + "GITHUB_EVENT_NAME": "push", "GITHUB_REF": "refs/heads/main", + "GITHUB_RUN_ID": "12345", "GITHUB_RUN_ATTEMPT": "2", "GITHUB_SHA": "a" * 40, +} +BASELINE = "b" * 40 +REQUIREMENTS = b"Jinja2==3.1.6\n" +PYTHON_TAG = "cpython-313" +URIS = ["'https://archive.example.test/a_1_amd64.deb' a_1_amd64.deb 42 MD5Sum:abc", + "'https://archive.example.test/b_2_amd64.deb' b_2_amd64.deb 64 MD5Sum:def"] +Y_FIELDS = ("environment-prefix", "tools-key", "pip-key") + + +def keys(profile="headless", *, environment=None, configuration=None, + baseline=BASELINE, requirements=REQUIREMENTS, python_tag=PYTHON_TAG): + return dependencies.environment_keys( + profile, baseline, requirements, python_tag=python_tag, + environment=ENVIRONMENT if environment is None else environment, + configuration=CONFIGURATION if configuration is None else configuration) + + +def apt_key(profile="headless", uris=URIS, *, environment=None, configuration=None): + return dependencies.apt_cache_key( + profile, uris, environment=ENVIRONMENT if environment is None else environment, + configuration=CONFIGURATION if configuration is None else configuration) + + +class IdentityTests(unittest.TestCase): + def test_prefix(self): + result = keys() + prefix = result["environment-prefix"] + self.assertTrue(prefix.startswith("actions-env-cache-v2-Linux-X64-headless-")) + for name in ("tools-key", "pip-key"): + self.assertTrue(result[name].startswith(prefix), name) + self.assertTrue(apt_key().startswith(prefix)) + + def test_revision(self): + changed = deepcopy(CONFIGURATION) + changed["cache_version"] = 3 + original = keys() + revised = keys(configuration=changed) + for name in Y_FIELDS: + self.assertNotEqual(original[name], revised[name], name) + self.assertTrue(revised[name].startswith("actions-env-cache-v3-"), name) + self.assertNotEqual(apt_key(), apt_key(configuration=changed)) + + def test_runner(self): + original = keys() + for name, value in (("RUNNER_OS", "Windows"), ("RUNNER_ARCH", "ARM64"), + ("ImageOS", "ubuntu26"), ("ImageVersion", "20261013.1.0")): + with self.subTest(name=name): + environment = {**ENVIRONMENT, name: value} + changed = keys(environment=environment) + for field in Y_FIELDS: + self.assertNotEqual(original[field], changed[field], field) + self.assertNotEqual(apt_key(), apt_key(environment=environment)) + + def test_composition(self): + prefixes = {keys(profile)["environment-prefix"] for profile in ("headless", "modules", "docs")} + self.assertEqual(len(prefixes), 3) + self.assertNotEqual(apt_key(), apt_key("modules")) + + def test_package_inputs(self): + changed = deepcopy(CONFIGURATION) + changed["linux"]["apt_common"].append("pkg-config") + original = keys() + revised = keys(configuration=changed) + for name in Y_FIELDS: + self.assertNotEqual(original[name], revised[name], name) + self.assertNotEqual(apt_key(), apt_key(configuration=changed)) + + def test_run_independent(self): + original = keys() + changes = {"GITHUB_REF": "refs/pull/42/merge", "GITHUB_EVENT_NAME": "pull_request", + "GITHUB_RUN_ID": "67890", "GITHUB_RUN_ATTEMPT": "9", "GITHUB_SHA": "c" * 40} + revised = keys(environment={**ENVIRONMENT, **changes}) + for name in Y_FIELDS: + self.assertEqual(original[name], revised[name], name) + self.assertEqual(apt_key(), apt_key(environment={**ENVIRONMENT, **changes})) + + def test_tool_versions(self): + environment = {**ENVIRONMENT, "RUNNER_OS": "Windows", "ImageOS": "win25"} + original = keys(environment=environment)["tools-key"] + self.assertNotEqual(original, keys(environment=environment, baseline="d" * 40)["tools-key"]) + for field, value in (("ccache_version", "4.15"), ("ccache_package_version", "4.15.0")): + with self.subTest(field=field): + changed = deepcopy(CONFIGURATION) + changed["windows"][field] = value + self.assertNotEqual(original, keys(environment=environment, configuration=changed)["tools-key"]) + + def test_python_inputs(self): + original = keys()["pip-key"] + self.assertNotEqual(original, keys(requirements=b"Jinja2==3.1.7\n")["pip-key"]) + self.assertNotEqual(original, keys(python_tag="cpython-314")["pip-key"]) + + def test_apt_downloads(self): + original = apt_key() + self.assertEqual(original, apt_key(uris=list(reversed(URIS)))) + self.assertNotEqual(original, apt_key(uris=[URIS[0]])) + changed = [URIS[0].replace("a_1_", "a_2_"), URIS[1]] + self.assertNotEqual(original, apt_key(uris=changed)) + self.assertNotEqual(original, apt_key(uris=[URIS[0].replace("MD5Sum:abc", "MD5Sum:xyz"), URIS[1]])) + + def test_pure_metadata(self): + configuration = deepcopy(CONFIGURATION) + environment = dict(ENVIRONMENT) + with patch.object(dependencies.subprocess, "run", side_effect=AssertionError("Unexpected command")), \ + patch.object(dependencies.subprocess, "check_output", side_effect=AssertionError("Unexpected Git query")): + keys(environment=environment, configuration=configuration) + apt_key(environment=environment, configuration=configuration) + self.assertEqual(configuration, CONFIGURATION) + self.assertEqual(environment, ENVIRONMENT) + + def test_invalid_revision(self): + for revision in (0, -1, True, "2", None): + with self.subTest(revision=revision): + configuration = {**CONFIGURATION, "cache_version": revision} + with self.assertRaises(ValueError): + keys(configuration=configuration) + + def test_invalid_runner(self): + for name, value in (("RUNNER_OS", "Linux\nextra=value"), ("RUNNER_ARCH", "../X64"), + ("RUNNER_OS", ""), ("RUNNER_ARCH", None)): + with self.subTest(name=name, value=value), self.assertRaises(ValueError): + keys(environment={**ENVIRONMENT, name: value}) + + def test_invalid_component(self): + for changes in ({"baseline": ""}, {"requirements": "Jinja2==3.1.6"}, + {"python_tag": ""}, {"python_tag": "cpython-313\nextra=value"}): + with self.subTest(changes=changes), self.assertRaises(ValueError): + keys(**changes) + for uris in ("one URI", [URIS[0], None]): + with self.subTest(uris=uris), self.assertRaises(ValueError): + apt_key(uris=uris) + + +class WriterTests(unittest.TestCase): + def writer(self, **changes): + return dependencies.environment_metadata( + "headless", environment={**ENVIRONMENT, **changes}, configuration=CONFIGURATION)["environment-cache-writer"] + + def test_main(self): + self.assertEqual(self.writer(), "true") + self.assertEqual(self.writer(GITHUB_EVENT_NAME="workflow_dispatch"), "true") + + def test_other_refs(self): + for ref in ("refs/heads/develop", "refs/heads/feature", "refs/pull/3/merge", "refs/tags/main", "main"): + with self.subTest(ref=ref): + self.assertEqual(self.writer(GITHUB_REF=ref), "false") + + def test_untrusted_events(self): + for event in ("pull_request", "pull_request_target", "workflow_run", "issue_comment", "repository_dispatch"): + with self.subTest(event=event): + self.assertEqual(self.writer(GITHUB_EVENT_NAME=event), "false") + + def test_other_repositories(self): + for repository in ("someone/cheryl-engine", "cppcooper/fork", ""): + with self.subTest(repository=repository): + self.assertEqual(self.writer(GITHUB_REPOSITORY=repository), "false") + + +class DependencyScopeTests(unittest.TestCase): + def scope(self, **changes): + return dependencies.dependency_scope(environment={**ENVIRONMENT, **changes}) + + def test_branch_writers(self): + for branch in ("main", "develop"): + for event in ("push", "workflow_dispatch"): + with self.subTest(branch=branch, event=event): + result = self.scope(GITHUB_REF=f"refs/heads/{branch}", GITHUB_EVENT_NAME=event) + self.assertEqual(result["dependency-role"], branch) + self.assertEqual(result["dependency-cache-writer"], "true") + + def test_pull_base(self): + for base in ("main", "develop"): + with self.subTest(base=base): + result = self.scope(GITHUB_EVENT_NAME="pull_request", GITHUB_REF="refs/pull/3/merge", + GITHUB_BASE_REF=base) + self.assertEqual(result["dependency-role"], base) + self.assertEqual(result["dependency-cache-writer"], "false") + + def test_no_pull_writer(self): + result = self.scope(GITHUB_EVENT_NAME="pull_request", GITHUB_REF="refs/heads/main", GITHUB_BASE_REF="main") + self.assertEqual(result["dependency-cache-writer"], "false") + + def test_no_other_writer(self): + changes = ({"GITHUB_REPOSITORY": "someone/cheryl-engine"}, + {"GITHUB_REF": "refs/heads/feature"}, {"GITHUB_REF": "refs/tags/main"}, + {"GITHUB_EVENT_NAME": "workflow_run"}) + for change in changes: + with self.subTest(change=change): + self.assertEqual(self.scope(**change)["dependency-cache-writer"], "false") + + +class CommandTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name).resolve() + directory = self.root / ".github/ci" + directory.mkdir(parents=True) + (directory / "environment.json").write_text(json.dumps(CONFIGURATION), encoding="utf-8") + (directory / "vcpkg.json").write_text(json.dumps({"builtin-baseline": BASELINE}), encoding="utf-8") + (directory / "requirements-modules.txt").write_bytes(REQUIREMENTS) + (self.root / ".gitmodules").write_text('[submodule "extern/example"]\n', encoding="utf-8") + self.environment = {**ENVIRONMENT, "RUNNER_TEMP": str(self.root / "runner-temp"), + "CHERYL_CI_DEPENDENCY_CACHE_DIR": str(self.root / "build/cache/dependencies")} + + def metadata(self, *, gitlinks=b"160000 commit aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\textern/example\0", changes=None): + emissions = {} + + def emit(variable, values): + emissions.setdefault(variable, {}).update(values) + + with patch.object(dependencies, "ROOT", self.root), \ + patch.dict(dependencies.os.environ, {**self.environment, **(changes or {})}, clear=True), \ + patch.object(dependencies.subprocess, "check_output", return_value=gitlinks), \ + patch.object(dependencies, "run", return_value=".git/modules\n"), \ + patch.object(dependencies, "emit", side_effect=emit): + dependencies.metadata("headless") + return emissions["GITHUB_OUTPUT"] + + def test_submodules_independent(self): + original = self.metadata() + (self.root / ".gitmodules").write_text('[submodule "extern/changed"]\n', encoding="utf-8") + changed = self.metadata(gitlinks=b"160000 commit dddddddddddddddddddddddddddddddddddddddd\textern/changed\0") + self.assertNotEqual(original["submodule-key"], changed["submodule-key"]) + for name in Y_FIELDS: + self.assertEqual(original[name], changed[name], name) + self.assertEqual(original["dependency-key"], changed["dependency-key"]) + + def test_branch_separation(self): + main = self.metadata() + develop = self.metadata(changes={"GITHUB_REF": "refs/heads/develop"}) + self.assertTrue(main["submodule-key"].startswith("build-deps-v2-Linux-X64-main-sources-")) + self.assertTrue(develop["submodule-key"].startswith("build-deps-v2-Linux-X64-develop-sources-")) + self.assertNotEqual(main["dependency-key"], develop["dependency-key"]) + self.assertEqual(main["submodule-main-prefix"], develop["submodule-main-prefix"]) + for name in Y_FIELDS: + self.assertEqual(main[name], develop[name], name) + self.assertEqual(develop["dependency-cache-writer"], "true") + self.assertEqual(develop["environment-cache-writer"], "false") + + def test_pull_metadata(self): + for base in ("main", "develop"): + with self.subTest(base=base): + branch = self.metadata(changes={"GITHUB_REF": f"refs/heads/{base}"}) + pull = self.metadata(changes={"GITHUB_EVENT_NAME": "pull_request", "GITHUB_REF": "refs/pull/3/merge", + "GITHUB_BASE_REF": base}) + self.assertEqual(branch["submodule-key"], pull["submodule-key"]) + self.assertEqual(branch["dependency-exact-prefix"], pull["dependency-exact-prefix"]) + self.assertEqual(pull["dependency-cache-writer"], "false") + self.assertEqual(pull["environment-cache-writer"], "false") + + def test_manifest_inputs(self): + original = self.metadata() + manifest = {"builtin-baseline": BASELINE, "dependencies": ["freetype", "harfbuzz"]} + (self.root / ".github/ci/vcpkg.json").write_text(json.dumps(manifest), encoding="utf-8") + changed = self.metadata() + self.assertNotEqual(original["dependency-key"], changed["dependency-key"]) + self.assertEqual(original["submodule-key"], changed["submodule-key"]) + for name in Y_FIELDS: + self.assertEqual(original[name], changed[name], name) + + def test_requirements_inputs(self): + original = self.metadata() + (self.root / ".github/ci/requirements-modules.txt").write_bytes(b"Jinja2==3.1.7\n") + changed = self.metadata() + self.assertNotEqual(original["pip-key"], changed["pip-key"]) + for name in ("environment-prefix", "tools-key", "submodule-key", "dependency-key"): + self.assertEqual(original[name], changed[name], name) + + def test_binary_environment(self): + original = self.metadata() + changed = self.metadata(changes={"ImageVersion": "20261013.1.0"}) + self.assertNotEqual(original["dependency-key"], changed["dependency-key"]) + + def test_metadata_runs(self): + original = self.metadata() + changed = self.metadata(changes={"GITHUB_REF": "refs/heads/develop", "GITHUB_RUN_ID": "99999", + "GITHUB_RUN_ATTEMPT": "3"}) + for name in Y_FIELDS: + self.assertEqual(original[name], changed[name], name) + self.assertEqual(original["environment-cache-writer"], "true") + self.assertEqual(changed["environment-cache-writer"], "false") + + def test_apt_plan(self): + output = "Reading package lists...\n" + "\n".join(reversed(URIS)) + "\n" + with patch.object(dependencies, "ROOT", self.root), \ + patch.dict(dependencies.os.environ, self.environment, clear=True), \ + patch.object(dependencies, "run", return_value=output) as command, \ + patch.object(dependencies, "emit") as emit: + dependencies.apt_plan("headless") + self.assertIn("--print-uris", command.call_args.args[0]) + self.assertEqual(command.call_args.kwargs["environment"]["LC_ALL"], "C") + self.assertEqual(emit.call_args.args, ("GITHUB_OUTPUT", { + "needed": "true", "key": apt_key(environment=self.environment)})) + + def test_apt_empty(self): + with patch.object(dependencies, "ROOT", self.root), \ + patch.dict(dependencies.os.environ, self.environment, clear=True), \ + patch.object(dependencies, "run", return_value="No packages to download.\n"), \ + patch.object(dependencies, "emit") as emit: + dependencies.apt_plan("modules") + self.assertEqual(emit.call_args.args[1]["needed"], "false") + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 663ab17e..5a73bb85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,7 @@ jobs: python .github/ci/test_ci.py python .github/ci/test_cache_cleanup.py python .github/ci/test_compiler_cache.py + python .github/ci/test_dependencies.py - name: Build and test Linux if: runner.os == 'Linux' env: @@ -78,11 +79,11 @@ jobs: python .github/ci/run.py --build-dir build/ci --profile headless --build-type $env:BUILD_TYPE --hwloc $env:HWLOC --vcpkg-root $env:CHERYL_CI_VCPKG_ROOT if ($LASTEXITCODE) { exit $LASTEXITCODE } - name: Check dependency cache updates - if: ${{ !cancelled() && runner.os == 'Windows' && steps.dependencies.outcome == 'success' }} + if: ${{ !cancelled() && runner.os == 'Windows' && steps.dependencies.outcome == 'success' && steps.dependencies.outputs.dependency-cache-writer == 'true' }} id: dependency-cache-state run: python .github/ci/dependencies.py cache-state - name: Save dependency packages and downloads - if: ${{ !cancelled() && steps.dependency-cache-state.outputs.changed == 'true' }} + if: ${{ !cancelled() && steps.dependencies.outputs.dependency-cache-writer == 'true' && steps.dependency-cache-state.outputs.changed == 'true' }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.CHERYL_CI_DEPENDENCY_CACHE_DIR }} @@ -165,11 +166,11 @@ jobs: python .github/ci/verify_sdk.py --packages build/packages --build-dir build/sdk-consumer --producer-build build/ci if ($LASTEXITCODE) { exit $LASTEXITCODE } - name: Check dependency cache updates - if: ${{ !cancelled() && runner.os == 'Windows' && steps.dependencies.outcome == 'success' }} + if: ${{ !cancelled() && runner.os == 'Windows' && steps.dependencies.outcome == 'success' && steps.dependencies.outputs.dependency-cache-writer == 'true' }} id: dependency-cache-state run: python .github/ci/dependencies.py cache-state - name: Save dependency packages and downloads - if: ${{ !cancelled() && steps.dependency-cache-state.outputs.changed == 'true' }} + if: ${{ !cancelled() && steps.dependencies.outputs.dependency-cache-writer == 'true' && steps.dependency-cache-state.outputs.changed == 'true' }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.CHERYL_CI_DEPENDENCY_CACHE_DIR }} diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 76c4588b..805e2c8a 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -44,15 +44,25 @@ jobs: - name: Identify documentation tool cache id: tool-cache run: python -c 'import sys; sys.path.insert(0, "docs/generation"); from bootstrap import environment_identity; print("identity=" + environment_identity())' >> "$GITHUB_OUTPUT" + - name: Describe shared Actions environment + id: environment + run: python .github/ci/dependencies.py environment-prefix --profile docs - name: Restore documentation tools - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: tools + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.CHERYL_DOCS_CACHE_DIR }} - key: docs-tools-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.tool-cache.outputs.identity }}-${{ hashFiles('docs/generation/requirements.txt', 'docs/generation/native-tools.json', 'docs/generation/bootstrap.py') }} + key: ${{ steps.environment.outputs.environment-prefix }}-tools-${{ steps.tool-cache.outputs.identity }}-${{ hashFiles('docs/generation/requirements.txt', 'docs/generation/native-tools.json', 'docs/generation/bootstrap.py') }} - name: Generate and check documentation run: | mkdir -p docs/generation/generated python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean 2>&1 | tee docs/generation/generated/generation.log + - name: Save shared documentation tools + if: steps.environment.outputs.environment-cache-writer == 'true' && steps.tools.outputs.cache-hit != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ env.CHERYL_DOCS_CACHE_DIR }} + key: ${{ steps.tools.outputs.cache-primary-key }} - name: Retain checked HTML uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: From f961924c6c6b011270b38229fe4ab75d77e2eb5a Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 16:05:04 -0700 Subject: [PATCH 20/98] Preserves completed documentation stages after validation failures Persist successful XML extraction independently of Sphinx, then persist completed rendering before running unconditional output validation. Checker failures still fail the command but retain doctrees and HTML for retry; Sphinx warning failures retain XML while requiring fresh rendering. Report specific rebuild reasons and per-stage elapsed time, preserve complete state for unchanged extraction-only requests, and document the distinction between reusable work and accepted output. Add simulated failure/retry and invalidation fixtures. Static syntax and whitespace checks passed; builds and tests were not run. --- docs/development/documentation.md | 16 +++- docs/generation/build.py | 139 ++++++++++++++++++---------- docs/generation/tests/test_build.py | 109 +++++++++++++++++++++- 3 files changed, 208 insertions(+), 56 deletions(-) diff --git a/docs/development/documentation.md b/docs/development/documentation.md index 5e98d986..81301cc7 100644 --- a/docs/development/documentation.md +++ b/docs/development/documentation.md @@ -96,7 +96,7 @@ renderer is selected; they are not rendered diagrams. The first generation performs the complete extraction and site build. Existing output created before incremental support also needs one complete generation to establish its cache state; the normal command handles this automatically. -Later runs reuse work from the last successful generation: +Later runs reuse completed stages: - Editing a guide reuses Doxygen XML, unchanged API pages and Sphinx doctrees. Sphinx rebuilds the affected pages. CSS/template changes rewrite HTML using @@ -107,9 +107,17 @@ Later runs reuse work from the last successful generation: invalidate reuse and require a full rebuild. Each run retains the warning, link, API-inventory and source-revision checks. -A failed generation is not recorded as valid cache state; fix its cause and run -the command again. Tool-installation caches are separate from generated-site -caches and remain reusable across clean generation. +A Sphinx failure preserves successful Doxygen extraction but requires fresh +rendering on retry. A later link or source-revision check failure preserves both +completed stages; fixing the cause and rerunning still performs all checks. +Cached rendering does not mean the output passed validation. Tool-installation +caches are separate from generated-site caches and survive clean generation. + +The log explains why XML or Sphinx output needs regeneration and reports the +time spent extracting, rendering and checking. `Generating fresh Sphinx output` +means the saved environment is being replaced; Sphinx then reports every guide +and generated API page as added. A changed Git revision requires fresh rendering +because source links are embedded in the cached document trees. Authored guides and included examples use Sphinx's normal modification-time tracking. If an external tool changes their contents while preserving or diff --git a/docs/generation/build.py b/docs/generation/build.py index 30589dac..f8cf60f0 100644 --- a/docs/generation/build.py +++ b/docs/generation/build.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +from contextlib import contextmanager import hashlib import json import os @@ -13,6 +14,7 @@ import shutil import subprocess import sys +import time import xml.etree.ElementTree as ElementTree # Set the cache location before importing repository-local modules. @@ -152,6 +154,15 @@ def fingerprint(value: dict) -> str: return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest() +@contextmanager +def timed_stage(name: str): + started = time.monotonic() + try: + yield + finally: + print(f"{name} took {time.monotonic() - started:.1f} seconds.", flush=True) + + def extraction_fingerprint(output: Path, doxygen: str, commit: str) -> str: return fingerprint({ "format": CACHE_FORMAT, @@ -166,12 +177,12 @@ def extraction_fingerprint(output: Path, doxygen: str, commit: str) -> str: }) -def rendering_fingerprint(output: Path, metadata: dict, xml_files: dict[str, str]) -> str: +def rendering_inputs(output: Path, metadata: dict, xml_files: dict[str, str]) -> dict: scripts = { path.relative_to(GENERATION_ROOT).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() for path in sorted([*GENERATION_ROOT.glob("*.py"), *(GENERATION_ROOT / "extensions").glob("*.py")]) } - return fingerprint({ + return { "format": CACHE_FORMAT, "repository": str(REPO_ROOT), "output": str(output), @@ -184,7 +195,11 @@ def rendering_fingerprint(output: Path, metadata: dict, xml_files: dict[str, str # Breathe records XML dependencies and cannot stat a removed compound. "xml_files": sorted(xml_files), "readthedocs_version": os.environ.get("READTHEDOCS_VERSION_NAME"), - }) + } + + +def rendering_fingerprint(output: Path, metadata: dict, xml_files: dict[str, str]) -> str: + return fingerprint(rendering_inputs(output, metadata, xml_files)) def presentation_fingerprint(metadata: dict) -> str: @@ -400,31 +415,58 @@ def main() -> int: and (output / "xml/index.xml").is_file() and not (output / ".xml-previous").exists() ) - # Sphinx can save doctrees before failing on warnings. Never reuse an - # interrupted/failed run as a successful baseline on the next invocation. - state_path.unlink(missing_ok=True) if reuse_xml: print("Reusing cached Doxygen XML.", flush=True) else: - print("Extracting public headers with Doxygen.", flush=True) - refresh_xml(output, doxygen, commit, incremental=not arguments.clean) + reasons = [] + if arguments.clean: + reasons.append("--clean requested") + if not state.get("extraction"): + reasons.append("no completed Doxygen stage") + elif state["extraction"] != extract_key: + reasons.append("header or extractor inputs changed") + if not xml_files or not (output / "xml/index.xml").is_file(): + reasons.append("cached XML is missing") + elif state.get("xml") != xml_files: + reasons.append("cached XML content changed") + if (output / ".xml-previous").exists(): + reasons.append("interrupted XML extraction") + print("Extracting public headers with Doxygen: " + "; ".join(reasons) + ".", flush=True) + with timed_stage("Doxygen extraction"): + refresh_xml(output, doxygen, commit, incremental=not arguments.clean) xml_files = file_hashes(output / "xml") next_state = {"format": CACHE_FORMAT, "extraction": extract_key, "xml": xml_files} + if arguments.extract_only and reuse_xml: + return 0 + # XML remains reusable when a later Sphinx or validation stage fails. + # Omit rendering until Sphinx succeeds: warning failures can save doctrees. + atomic_json(state_path, next_state) if arguments.extract_only: - atomic_json(state_path, next_state) return 0 metadata = provenance(commit, doxygen_version, output) - render_key = rendering_fingerprint(output, metadata, xml_files) + render_inputs = rendering_inputs(output, metadata, xml_files) + render_key = fingerprint(render_inputs) presentation_key = presentation_fingerprint(metadata) - fresh = ( - arguments.clean - or state.get("rendering") != render_key - or not (output / "doctrees/environment.pickle").is_file() - or not (output / "html/index.html").is_file() - ) + reasons = [] + if arguments.clean: + reasons.append("--clean requested") + if not state.get("rendering"): + reasons.append("no completed Sphinx stage") + elif state["rendering"] != render_key: + previous = state.get("rendering_inputs") + changed = [] + if isinstance(previous, dict): + changed = [name for name in render_inputs if previous.get(name) != render_inputs[name]] + labels = ", ".join(changed or ["revision/config/tools/XML inventory"]) + reasons.append(f"rendering inputs changed ({labels})") + if not (output / "doctrees/environment.pickle").is_file(): + reasons.append("cached Sphinx environment is missing") + if not (output / "html/index.html").is_file(): + reasons.append("cached HTML entry point is missing") + fresh = bool(reasons) if fresh: - print("Generating fresh Sphinx output.", flush=True) + print("Generating fresh Sphinx output: " + "; ".join(reasons) + ".", flush=True) for directory in (GENERATED_ROOT / "api", GENERATED_ROOT / ".api-previous", output / "html", output / "doctrees"): if directory.exists(): @@ -440,40 +482,43 @@ def main() -> int: # CSS/template changes and the dirty-source banner affect every HTML # page, but do not require reparsing unchanged source documents. write_all = ["-a"] if not fresh and state.get("presentation") != presentation_key else [] - subprocess.run( - [ - sys.executable, - "-m", - "sphinx", - "-c", - str(GENERATION_ROOT), - "-b", - "html", - "-W", - "--keep-going", - "-n", - *write_all, - "-w", - str(output / "sphinx-warnings.log"), - "-d", - str(output / "doctrees"), - str(REPO_ROOT / "docs"), - str(output / "html"), - ], - cwd=REPO_ROOT, - env=environment, - check=True, - ) + with timed_stage("Sphinx rendering"): + subprocess.run( + [ + sys.executable, + "-m", + "sphinx", + "-c", + str(GENERATION_ROOT), + "-b", + "html", + "-W", + "--keep-going", + "-n", + *write_all, + "-w", + str(output / "sphinx-warnings.log"), + "-d", + str(output / "doctrees"), + str(REPO_ROOT / "docs"), + str(output / "html"), + ], + cwd=REPO_ROOT, + env=environment, + check=True, + ) (output / "html/revision.json").write_text( json.dumps(metadata, indent=2, sort_keys=True) + "\n", encoding="utf-8" ) - subprocess.run( - [sys.executable, str(Path(__file__).with_name("check.py")), - "--html", str(output / "html"), "--xml", str(output / "xml")], - cwd=REPO_ROOT, check=True, - ) - next_state.update(rendering=render_key, presentation=presentation_key) + next_state.update(rendering=render_key, rendering_inputs=render_inputs, presentation=presentation_key) atomic_json(state_path, next_state) + # Validation always runs, including retries against completed Sphinx output. + with timed_stage("Documentation checks"): + subprocess.run( + [sys.executable, str(Path(__file__).with_name("check.py")), + "--html", str(output / "html"), "--xml", str(output / "xml")], + cwd=REPO_ROOT, check=True, + ) return 0 diff --git a/docs/generation/tests/test_build.py b/docs/generation/tests/test_build.py index 5480fd46..d8c56c8f 100644 --- a/docs/generation/tests/test_build.py +++ b/docs/generation/tests/test_build.py @@ -5,6 +5,7 @@ from contextlib import nullcontext, redirect_stderr, redirect_stdout import importlib.util import io +import json import os from pathlib import Path import subprocess @@ -74,7 +75,10 @@ def setUp(self): } self.sphinx_runs = [] self.check_runs = [] + self.check_states = [] self.fail_sphinx = False + self.fail_check = False + self.last_output = "" self.enterContext(patch.dict(os.environ, { "CHERYL_DOCS_TOOLS_ACTIVE": "1", "CHERYL_DOCS_DOXYGEN": str(self.doxygen), @@ -123,14 +127,22 @@ def run_tool(self, command, **options): raise subprocess.CalledProcessError(1, command) elif len(command) > 1 and Path(command[1]) == self.tools / "check.py": self.check_runs.append(command) + self.check_states.append(json.loads(self.state.read_text(encoding="utf-8"))) + if self.fail_check: + self.fail_check = False + raise subprocess.CalledProcessError(1, command) else: raise AssertionError(f"Unexpected external tool: {command!r}") return subprocess.CompletedProcess(command, 0) def run_build(self, *arguments): - with patch.object(sys, "argv", [str(self.tools / "build.py"), *arguments]), \ - redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()): - return builder.main() + output = io.StringIO() + try: + with patch.object(sys, "argv", [str(self.tools / "build.py"), *arguments]), \ + redirect_stdout(output), redirect_stderr(io.StringIO()): + return builder.main() + finally: + self.last_output = output.getvalue() def retain_markers(self): self.html_marker.write_text("Existing HTML\n", encoding="utf-8") @@ -150,6 +162,7 @@ def test_warm_reuse(self): self.assertNotIn("-a", self.sphinx_runs[-1]["command"]) self.assertEqual(len(self.check_runs), 2) self.assertTrue(self.state.is_file()) + self.assertIn("Updating Sphinx output incrementally.", self.last_output) def test_header_change(self): self.run_build() @@ -200,17 +213,102 @@ def test_failed_run_retries(self): self.fail_sphinx = True with self.assertRaises(subprocess.CalledProcessError): self.run_build() - self.assertFalse(self.state.exists()) + state = json.loads(self.state.read_text(encoding="utf-8")) + self.assertIn("extraction", state) + self.assertNotIn("rendering", state) + self.assertNotIn("presentation", state) self.assertEqual((self.output / "doctrees/environment.pickle").read_bytes(), b"environment-2") self.run_build() - self.assertEqual(self.extractor.call_count, 2) + self.assertEqual(self.extractor.call_count, 1) self.assertIsNone(self.sphinx_runs[-1]["environment"]) self.assertFalse(self.sphinx_runs[-1]["html_retained"]) self.assertFalse(self.sphinx_runs[-1]["doctrees_retained"]) self.assertEqual(len(self.check_runs), 2) self.assertTrue(self.state.is_file()) + self.assertIn("no completed Sphinx stage", self.last_output) + + def test_failed_checks_reuse(self): + self.fail_check = True + with self.assertRaises(subprocess.CalledProcessError): + self.run_build() + state = json.loads(self.state.read_text(encoding="utf-8")) + self.assertIn("rendering", state) + self.assertIn("presentation", state) + self.assertIn("rendering", self.check_states[-1]) + self.retain_markers() + + self.run_build() + + self.assertEqual(self.extractor.call_count, 1) + self.assertEqual(len(self.check_runs), 2) + self.assertEqual(self.sphinx_runs[-1]["environment"], b"environment-1") + self.assertTrue(self.sphinx_runs[-1]["html_retained"]) + self.assertTrue(self.sphinx_runs[-1]["doctrees_retained"]) + self.assertIn("Updating Sphinx output incrementally.", self.last_output) + + def test_extract_only(self): + self.run_build("--extract-only") + state = json.loads(self.state.read_text(encoding="utf-8")) + self.assertIn("extraction", state) + self.assertNotIn("rendering", state) + self.assertEqual(self.sphinx_runs, []) + self.assertEqual(self.check_runs, []) + + self.run_build() + + self.assertEqual(self.extractor.call_count, 1) + self.assertEqual(len(self.check_runs), 1) + self.assertIn("Reusing cached Doxygen XML.", self.last_output) + + def test_warm_extract_only(self): + self.run_build() + original = self.state.read_bytes() + + self.run_build("--extract-only") + + self.assertEqual(self.state.read_bytes(), original) + self.assertEqual(self.extractor.call_count, 1) + self.assertEqual(len(self.sphinx_runs), 1) + self.assertEqual(len(self.check_runs), 1) + + def test_initial_reasons(self): + self.run_build() + + for reason in ("no completed Doxygen stage", "no completed Sphinx stage", + "cached Sphinx environment is missing", "cached HTML entry point is missing", + "Doxygen extraction took", "Sphinx rendering took", "Documentation checks took"): + self.assertIn(reason, self.last_output) + + def test_revision_reason(self): + self.run_build() + self.metadata["source_revision"] = "2" * 40 + + with patch.object(builder, "revision", return_value=self.metadata["source_revision"]): + self.run_build() + + self.assertIn("rendering inputs changed (revision)", self.last_output) + + def test_missing_environment(self): + self.run_build() + (self.output / "doctrees/environment.pickle").unlink() + + self.run_build() + + self.assertEqual(self.extractor.call_count, 1) + self.assertIsNone(self.sphinx_runs[-1]["environment"]) + self.assertIn("cached Sphinx environment is missing", self.last_output) + + def test_missing_html(self): + self.run_build() + (self.output / "html/index.html").unlink() + + self.run_build() + + self.assertEqual(self.extractor.call_count, 1) + self.assertIsNone(self.sphinx_runs[-1]["environment"]) + self.assertIn("cached HTML entry point is missing", self.last_output) def test_clean(self): self.run_build() @@ -223,6 +321,7 @@ def test_clean(self): self.assertFalse(self.sphinx_runs[-1]["html_retained"]) self.assertFalse(self.sphinx_runs[-1]["doctrees_retained"]) self.assertEqual(self.sphinx_runs[-1]["incremental"], "0") + self.assertIn("--clean requested", self.last_output) def test_xml_preserves_unchanged(self): self.documents["obsolete.xml"] = "\n" From b2c9f86017c62ddfc36b248ac63c28a8e00ab958 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 16:06:33 -0700 Subject: [PATCH 21/98] Documents cache ownership and preserves reusable environment setup Prepare and publish shared documentation tools before document generation so validation failures do not discard a valid environment. Prefer the newest immutable X source snapshot for each main/develop runner while leaving historical source pins available under budget. Document separate compiler, branch dependency and shared versioned environment families, invalidation inputs, publish ownership, cache-version updates and bounded eviction. Extend source-retention fixtures. Static workflow lint, syntax and whitespace checks passed; builds and tests were not run. --- .github/ci/cache_cleanup.py | 6 ++-- .github/ci/test_cache_cleanup.py | 28 +++++++++++++++ .github/workflows/documentation.yml | 10 +++--- docs/development/building.md | 55 +++++++++++++++++++++++------ 4 files changed, 83 insertions(+), 16 deletions(-) diff --git a/.github/ci/cache_cleanup.py b/.github/ci/cache_cleanup.py index a9faec3b..c59f9bb4 100644 --- a/.github/ci/cache_cleanup.py +++ b/.github/ci/cache_cleanup.py @@ -21,6 +21,7 @@ KNOWN_PREFIXES = ("ccache-v1-", "ccache-v2-", "dependencies-v1-", "pip-v1-", "docs-tools-v1-", "windows-tools-v1-", "submodule-git-v1-", "apt-debs-v1-", "build-deps-v2-") ENVIRONMENT_KEY = re.compile(r"actions-env-cache-v[1-9][0-9]*-.+") +SOURCE_KEY = re.compile(r"(build-deps-v2-.+-(?:main|develop)-sources-)[0-9a-f]{64}") MAINTENANCE_PATH = ".github/workflows/cache-maintenance.yml" @@ -105,6 +106,7 @@ def remove(cache, reason): baseline_policy = {} for record in entries: cache, created, _, logical = record + source = SOURCE_KEY.fullmatch(cache["key"]) if logical is not None: bucket = (cache["ref"], cache["version"], logical) if bucket not in newest or (created, cache["id"]) > (newest[bucket][1], newest[bucket][0]["id"]): @@ -115,7 +117,7 @@ def remove(cache, reason): known = cache["key"].startswith(KNOWN_PREFIXES) or ENVIRONMENT_KEY.fullmatch(cache["key"]) if cache["ref"] == "refs/heads/" + default_branch and known: priority, reason = 2, "default-branch baseline" - if logical is not None and cache["key"].startswith(("ccache-v1-", "ccache-v2-", "build-deps-v2-")): + if source or (logical is not None and cache["key"].startswith(("ccache-v1-", "ccache-v2-", "build-deps-v2-"))): match = PULL_REF.fullmatch(cache["ref"]) if cache["ref"] == "refs/heads/main": priority, reason = 3, "main baseline" @@ -124,7 +126,7 @@ def remove(cache, reason): elif match and int(match.group(1)) in open_prs: priority, reason = 1, "open pull request baseline" if priority: - bucket = (cache["ref"], cache["version"], logical or cache["key"]) + bucket = (cache["ref"], cache["version"], source.group(1) if source else logical or cache["key"]) if bucket not in baseline or (created, cache["id"]) > (baseline[bucket][1], baseline[bucket][0]["id"]): baseline[bucket] = record baseline_policy[bucket] = (priority, reason) diff --git a/.github/ci/test_cache_cleanup.py b/.github/ci/test_cache_cleanup.py index 4821381d..a4db4733 100644 --- a/.github/ci/test_cache_cleanup.py +++ b/.github/ci/test_cache_cleanup.py @@ -82,6 +82,7 @@ def test_lineages(self): "unrelated-12345-2", f"{COMPILER}-12345-retry", "build-deps-v2-Linux-X64-main-sources-12345-2", + "build-deps-v2-Linux-X64-main-sources-" + "a" * 64, "actions-env-cache-v2-Linux-X64-modules-identity-tools-12345-2", ): with self.subTest(key=key): @@ -118,6 +119,33 @@ def test_build_sources(self): cache(2, key=prefix + "b" * 64, ref="refs/heads/main")] self.assertEqual(removed(plan(rows)), {}) + def test_source_priorities(self): + main = "build-deps-v2-Linux-X64-main-sources-" + develop = main.replace("-main-", "-develop-") + rows = [cache(1, key=main + "a" * 64, ref="refs/heads/main", size=GIB, accessed=1800), + cache(2, key=main + "b" * 64, ref="refs/heads/main", size=GIB, age=7200, accessed=18000), + cache(3, key=develop + "a" * 64, size=GIB, accessed=1800), + cache(4, key=develop + "b" * 64, size=GIB, age=7200, accessed=14400), + cache(5, key="ordinary", ref="refs/heads/feature", size=GIB)] + result = plan(rows, default_branch="main", budget_bytes=2 * GIB) + self.assertEqual(removed(result), {1: "least recently used", 3: "least recently used", + 5: "least recently used"}) + result = plan(rows, default_branch="main", budget_bytes=GIB) + self.assertEqual(removed(result), {1: "least recently used", 3: "least recently used", + 5: "least recently used", 4: "develop baseline"}) + + def test_source_isolation(self): + prefix = "build-deps-v2-Linux-X64-main-sources-" + rows = [cache(1, key=prefix + "a" * 64, ref="refs/heads/main", size=GIB), + cache(2, key=prefix + "b" * 64, ref="refs/heads/main", size=GIB, age=7200), + cache(3, key=prefix.replace("Linux-X64", "Windows-X64") + "a" * 64, + ref="refs/heads/main", size=GIB, accessed=18000), + cache(4, key=prefix + "a" * 64, size=GIB, accessed=14400), + cache(5, key=prefix + "a" * 64, ref="refs/heads/main", version="other-path", + size=GIB, accessed=10800)] + result = plan(rows, default_branch="main", budget_bytes=4 * GIB) + self.assertEqual(removed(result), {1: "least recently used"}) + def test_build_compaction(self): rows = [cache(1, key=f"{BUILD_DEPS}-1-1", ref="refs/heads/main", age=18000), cache(2, key=f"{BUILD_DEPS}-2-1", ref="refs/heads/main", age=7200)] diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 805e2c8a..566467d8 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -53,16 +53,18 @@ jobs: with: path: ${{ env.CHERYL_DOCS_CACHE_DIR }} key: ${{ steps.environment.outputs.environment-prefix }}-tools-${{ steps.tool-cache.outputs.identity }}-${{ hashFiles('docs/generation/requirements.txt', 'docs/generation/native-tools.json', 'docs/generation/bootstrap.py') }} - - name: Generate and check documentation - run: | - mkdir -p docs/generation/generated - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean 2>&1 | tee docs/generation/generated/generation.log + - name: Prepare pinned documentation tools + run: python docs/generation/bootstrap.py setup --force-pinned - name: Save shared documentation tools if: steps.environment.outputs.environment-cache-writer == 'true' && steps.tools.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.CHERYL_DOCS_CACHE_DIR }} key: ${{ steps.tools.outputs.cache-primary-key }} + - name: Generate and check documentation + run: | + mkdir -p docs/generation/generated + python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean 2>&1 | tee docs/generation/generated/generation.log - name: Retain checked HTML uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: diff --git a/docs/development/building.md b/docs/development/building.md index 2deb96f7..35a84d99 100644 --- a/docs/development/building.md +++ b/docs/development/building.md @@ -368,8 +368,15 @@ intentionally skipped on PR, `develop` and manual runs. ### Actions cache retention Compiler and dependency caches speed up subsequent CI runs. They are separate -from the SDK and diagnostic artifacts above. Each compiler matrix configuration -has separate ccache layers: +from the SDK and diagnostic artifacts above, and from each other: + +| Cache | Key family | Contents and ownership | +| --- | --- | --- | +| Compiler results | `ccache-v2` | Main, develop and PR layers, described below. | +| X: build dependencies | `build-deps-v2` | Separate main/develop submodule Git data and Windows vcpkg package archives/downloads. PRs read their base branch's X. | +| Y: Actions environment | `actions-env-cache-v2` | Shared tools, Linux package downloads, Python wheels/downloads and prepared documentation tools, published by main. | + +Each compiler matrix configuration has separate ccache layers: | Build | Readable layers | Writable layer saved after building | | --- | --- | --- | @@ -398,20 +405,48 @@ writable layer and the main baseline. These are Missing parents cause normal cache misses until their builds populate them. The `ccache-v2` keys distinguish this file-backend layout from the previous ordinary local-cache archives; the first build of each configuration seeds the -new format. Windows dependency and pip caches continue using full snapshots. -All updated snapshots use new run-specific keys because saved GitHub caches are -immutable. +new format. Compiler layers and Windows binary dependency snapshots use new +run-specific keys because saved GitHub caches are immutable. + +Cache X's source key hashes `.gitmodules` and the recorded submodule commits. +Updating a submodule selects a new source snapshot automatically; an older X can +seed Git objects before checkout fetches the requested commits. The binary +component follows the vcpkg manifest and runner image, with vcpkg validating each +package's binary compatibility when using a fallback snapshot. Vendored libraries +remain ordinary CMake targets and reuse their compiler results through ccache. +Only main/develop push and manual runs publish X; PRs fetch or build missing +dependencies locally. + +To update Y, change package/tool selections in +[environment.json](../../.github/ci/environment.json), generator packages in +[requirements-modules.txt](../../.github/ci/requirements-modules.txt), or the +documentation tool manifests. Bump `cache_version` from `2` to `3` when revising +the Actions environment or forcing a refresh; this changes the family to +`actions-env-cache-v3`. Environment inputs are also hashed so package changes +cannot silently reuse a mismatched key. Y has separate immutable components for +each OS/architecture, runner image and environment composition. Resolved Linux +package versions, Python ABI and tool manifests further identify their payloads; +submodule commits and run IDs do not affect Y. + +Every branch restores Y, while only trusted main push/manual runs publish it. +Develop and PR changes to the environment install normally on a cache miss; +main publishes the updated Y after those changes reach it. Package installers +still run on each fresh runner; cached downloads avoid fetching the same packages +again. Documentation tooling is prepared and cached before document generation, +so a later documentation failure does not discard the prepared environment. [cache-maintenance.yml](../../.github/workflows/cache-maintenance.yml) runs after the build or documentation workflow finishes and daily at 04:23 UTC. It removes -caches belonging to closed pull requests, keeps the newest compiler/dependency/pip -snapshot per branch, configuration and cache version, and trims older caches +caches belonging to closed pull requests, keeps the newest mutable compiler and +dependency snapshot per branch, configuration and cache version, and trims older caches toward an 8 GiB repository budget. Active workflow refs and caches created or accessed within the last 15 minutes are protected. After removing redundant snapshots, budget eviction removes ordinary old caches first, then the latest -open-PR compiler layers, then develop layers, and finally main layers. Known -default-branch tooling caches have the same priority as develop layers. Each -tier uses oldest access first. Old-format compiler snapshots lose their branch +open-PR compiler/dependency snapshots, then develop snapshots, and finally main +snapshots. Known default-branch tooling caches have the same priority as develop +layers. Each tier uses oldest access first. The newest X source snapshot per +branch and runner receives that branch's priority; older submodule pins remain +ordinary eviction candidates. Old-format compiler snapshots lose their branch priority once the same branch and configuration has a new-format replacement. These priorities preserve useful branch caches where space allows; the budget can still require evicting a current layer. The budget is a target: concurrent From d6b99d3612b9e221a9077d22ea24728f52101f93 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Thu, 15 Oct 2026 23:01:41 -0700 Subject: [PATCH 22/98] Merges branch feat-shared-libraries into fixes-develop (squash) * Fixes invalid font sources before sizing their contents Rejects nonregular explicit font paths before opening or seeking them. Linux directory streams can report an enormous size and previously reached an impossible allocation instead of the documented font-loading error. Keeps missing-file skipping and genuine allocation failures intact, and labels the corrupt-font regression fixtures. * Fixes concurrent font fixture directory collisions Adds random entropy and bounded atomic directory-creation retries to font selection fixtures. Parallel CTest processes can share clock values and process-local counters, particularly on Windows; an existing directory now causes a retry rather than failing the test or taking ownership of another fixture. Preserves real filesystem errors and cleanup of successfully claimed directories. The focused font selection cases pass in both static and shared Linux builds. * Adds selectable shared Engine and module libraries Adds CHERYL_BUILD_SHARED_LIBS while retaining static libraries by default. Exports Engine APIs through CE_API and gives modules/support independent import/export definitions. Versions static archives and Windows DLL names, and sets ELF SONAME versions and relocatable runtime paths. Centralizes built-in singleton, logging, cache-provider and crash-bootstrap state in its owning library. Builds stateful owned dependencies with matching shared linkage and preserves supplied dependency output settings. Packages matched static/shared SDKs with component-version and exact artifact metadata, preserving safe relative SONAME symlinks. Adds relocated consumer checks for shared state and exceptions across a library boundary and expands Linux/Windows CI to cover both linkage choices. Updates existing build and consumer guidance for deployment and binary compatibility constraints. Validates Linux static/shared SDK, module, demo and first-game builds; all 11 font cases and all eight relocated SDK consumers pass in each mode. CI tooling checks pass, as do an MSVC public-header compile and a bounded Windows DLL state-sharing probe. Full Windows SDK verification remains with hosted CI. * Centralizes the shared-library build option Declares CHERYL_BUILD_SHARED_LIBS in CherylOptions alongside the other build options. Root and standalone library entry points use the same option configuration helper; linkage policy remains in CherylLibraries. Verifies static/shared root configuration and standalone library configuration without compiling. * Simplifies Linux terminal build selection Calculates CHERYL_BUILD_DEBUG_TERMINAL_LINUX directly in cheryl_configure_root_options: supported single-configuration Linux Debug builds default to ON, while other configurations default to OFF. Explicit ON includes the terminal in all configurations, including multi-config builds. Makes the source module consume that boolean directly and removes the separate CHERYL_DEBUG_TERMINAL policy/helper. Keeps Debug automatic display and explicit Release runtime activation unchanged. Updates build, module and demo guidance for standalone selection, cached values and configuration policy. Verifies root defaults, explicit overrides, unsupported platform/compiler defaults, multi-config selection, standalone Engine bootstrap and SDK-based source-module composition through configuration and generated linker checks. * Removes the deprecated sandbox build option Deletes CHERYL_SANDBOX_BUILD and its Native GLFW default overrides. Native input defaults to ON and the null platform to OFF; callers select those capabilities explicitly through CHERYL_NATIVE_INPUT and CHERYL_NATIVE_NULL_PLATFORM. Removes obsolete documentation flags and shorthand references while preserving explicit platform/input coverage. Fresh desktop and explicit null-platform configurations generate successfully without declaring a sandbox cache option. * Centralizes Cheryl build option declarations Moves module, native, logging, topology, documentation, fixture and example option defaults into CherylOptions.cmake. Entry points request their own option groups, preserving standalone defaults and caller overrides. Declares source selectors centrally and keeps the option summary singular and deferred until an actual boolean is declared. Ships the option definitions with the SDK and lets first-game obtain them from the checkout or installed package while retaining its older-SDK fallback. Removes CHERYL_RMLUI_PLACEHOLDER_FIX_VERIFIED. Owned RmlUi source still receives the placeholder correction; supplied libraries must declare the correction on their actual Core target. Updates the existing dependency and build guidance. Checks static/shared root configuration, standalone RmlUi, documentation tool arguments, first-game source and old/new SDK consumption, plus marked/unmarked RmlUi target acceptance. Verifies no owned cache declarations remain outside the central options file. No runtime implementation changes are required. * Fixes documentation extraction of shared-library exports Enables selective Doxygen preprocessing and erases Engine, component and RTTI visibility decorators before generating XML. Their unexpanded tokens were being parsed as C++ types, producing 473 strict Sphinx warnings in the shared-library PR, including a phantom BitmapFont variable. Keeps unrelated macros unexpanded and retains the native-input reference selection. Adds configuration regressions that compare the decorator list against export.h so future component annotations cannot silently leak into documented declarations. Validates public XML extraction, all 80 documentation-tooling tests and 18 publication tests. Existing CI already executes these checks and performs a strict complete HTML build. * Advances college-port assets for named sprites and HUD layouts Records the assets revision containing gameplay-oriented Space Invaders sprite names and RmlUi HUD layouts for the college projects. * Updates default assemblies and preserves isolated build profiles Selects shared libraries, documentation and SDK targets, both worker providers, RmlUi and miniaudio by default while making the demo and tests opt-in. Keeps documentation disabled in ordinary CI build profiles and suppresses unrelated options in standalone module and source-consumer Engine bootstraps. Synchronizes default values, prerequisites, demo and static-build commands, and Windows source-consumer DLL setup in the existing guides. * Fixes Windows shared GLFW callback scope exports Keeps the callback-depth thread-local counter private to the Native GLFW library and exports out-of-line scope construction, destruction and active-state queries instead. This avoids MSVC C2492 while preserving one callback-depth value per thread across DLL consumers. * Adds documentation theme controls and responsive content width Follows the browser colour preference by default and lets readers select and persist light or dark mode. Adds a screen-only dark palette for navigation, API signatures, syntax highlighting, tables and notices while preserving light print output. Expands the content panel to 75 percent of the viewport, retaining the original 800-pixel width when available and fitting smaller windows. Allows nested API field values to wrap without overflowing narrow viewports. * Fixes missing Gainput device-map exports in Windows shared builds Explicitly instantiates the device-map insertion operator in the owned Windows Gainput DLL so consumer-side device factories can resolve its import. Keeps supplied Gainput targets and other configurations unchanged. Exercises the same map lookup in the Native GLFW source and installed-SDK consumer without initializing a window or device. * Consolidates compiler caches into shared family pools Shares Linux GCC, Linux Clang and Windows MSVC results across compatible jobs while preserving main, develop and PR cache layers. Workers upload isolated deltas; one publisher unions results, keeps complete manifests, subtracts parent entries and bounds each pool at 1 GiB. Imports compatible per-job v2 snapshots when a v3 pool is absent, retires replaced legacy snapshots within the same ref, and preserves contributions across failed-job reruns. Validates archive contents and adds focused regression coverage. * Separates CI compilation from test execution Adds explicit build and test phases to the composition and relocated SDK runners. Keeps the combined default for existing callers while CI uses distinct build and test steps on the same runner, without repeating compilation or test execution. Preserves test fixtures, selected suites and Windows DLL search paths. Checks phase command selection and reuse of the previously extracted SDK. --- .../actions/migrate-compiler-cache/action.yml | 26 ++ .../actions/restore-compiler-cache/action.yml | 23 +- .github/ci/cache_cleanup.py | 52 ++- .github/ci/compiler_cache.py | 223 ++++++++-- .github/ci/package.py | 50 ++- .github/ci/run.py | 19 +- .github/ci/sdk-consumer/CMakeLists.txt | 19 + .github/ci/sdk-consumer/state-bridge.cpp | 21 + .github/ci/sdk-consumer/state-bridge.h | 22 + .github/ci/sdk-consumer/state.cpp | 43 ++ .github/ci/test_cache_cleanup.py | 77 +++- .github/ci/test_ci.py | 168 ++++++- .github/ci/test_compiler_cache.py | 419 +++++++++++++++--- .github/ci/verify_sdk.py | 73 +-- .github/workflows/ci.yml | 193 ++++++-- CMakeLists.txt | 1 + README.md | 16 +- assets | 2 +- cmake/CherylDependencies.cmake | 12 +- cmake/CherylDocumentation.cmake | 5 +- cmake/CherylLibraries.cmake | 71 +++ cmake/CherylModules.cmake | 7 +- cmake/CherylOptions.cmake | 109 +++-- cmake/CherylSdk.cmake | 37 +- cmake/templates/CherylConfig.cmake.in | 10 +- cmake/templates/CherylSdk.json.in | 4 + cmake/templates/Module.cmake.in | 2 +- docs/STATUS.md | 4 +- docs/development/architecture-validation.md | 16 +- docs/development/building.md | 99 +++-- docs/development/consuming-engine.md | 89 +++- docs/development/logging-acceptance.md | 4 +- docs/development/modules.md | 14 +- docs/development/native-desktop-checks.md | 4 +- docs/development/ui-adapters.md | 5 +- docs/external-work/rmlui-placeholder-issue.md | 2 +- docs/generation/Doxyfile.in | 12 +- docs/generation/conf.py | 3 +- docs/generation/static/appearance.js | 49 ++ docs/generation/static/layout.css | 18 + docs/generation/static/theme.css | 409 +++++++++++++++++ docs/generation/templates/layout.html | 8 + docs/generation/tests/test_configuration.py | 40 ++ docs/getting-started/project-setup.md | 23 +- .../planning/long-term/platform-acceptance.md | 5 +- docs/runtime/worker-execution.md | 4 +- projects/apps/demo/README.md | 23 +- projects/apps/first-game/CMakeLists.txt | 13 +- projects/engine/CMakeLists.txt | 13 +- .../cheryl/assets/definitions/animation.h | 4 +- .../include/cheryl/assets/definitions/grid.h | 12 +- .../cheryl/assets/definitions/manifest.h | 4 +- .../cheryl/assets/definitions/pipeline.h | 4 +- .../cheryl/assets/definitions/shader-assets.h | 15 +- .../cheryl/assets/definitions/sprite.h | 4 +- .../cheryl/assets/definitions/tileset.h | 6 +- .../cheryl/assets/geometry/grid-geometry.h | 4 +- .../cheryl/assets/resources/decoded-image.h | 4 +- .../cheryl/assets/resources/geometry2d.h | 4 +- .../include/cheryl/assets/resources/image.h | 4 +- .../cheryl/assets/resources/parameters.h | 12 +- .../cheryl/assets/resources/pipeline.h | 14 +- .../assets/resources/resource-provider.h | 4 +- .../include/cheryl/assets/resources/sampler.h | 6 +- .../assets/resources/shader-asset-builder.h | 4 +- .../include/cheryl/assets/resources/shader.h | 4 +- .../cheryl/assets/selection/tile-selection.h | 8 +- .../include/cheryl/assets/submission/draw2d.h | 19 +- .../include/cheryl/assets/types/2d/ffont.h | 24 +- .../include/cheryl/assets/types/2d/font.h | 6 +- .../include/cheryl/assets/types/2d/graphic.h | 6 +- .../include/cheryl/assets/types/2d/sprite.h | 28 +- .../include/cheryl/assets/types/2d/stbfont.h | 11 +- .../include/cheryl/assets/types/2d/tileset.h | 25 +- .../cheryl/assets/types/2d/unicode-text.h | 16 +- projects/engine/include/cheryl/audio/clip.h | 16 +- projects/engine/include/cheryl/audio/system.h | 10 +- .../cheryl/core/controls/input-accumulator.h | 4 +- .../cheryl/core/controls/input-bindings.h | 38 +- .../cheryl/core/controls/input-capture.h | 22 +- .../cheryl/core/controls/input-interface.h | 19 +- .../cheryl/core/controls/input-routing.h | 16 +- .../cheryl/core/controls/polling-backlog.h | 16 +- .../include/cheryl/core/controls/tick-input.h | 12 +- .../engine/include/cheryl/core/diagnostics.h | 4 +- .../core/display/display-system-interface.h | 4 +- .../include/cheryl/core/display/monitor.h | 4 +- .../cheryl/core/display/window-interface.h | 4 +- .../cheryl/core/engine/engine-context.h | 32 +- .../cheryl/core/engine/event-delivery.h | 8 +- .../cheryl/core/engine/platform-dispatcher.h | 20 +- .../core/engine/simulation-dispatcher.h | 22 +- .../cheryl/core/engine/worker-configuration.h | 6 +- .../include/cheryl/core/engine/worker-pool.h | 44 +- .../cheryl/core/engine/worker-topology.h | 4 +- .../core/game-framework/abstract-game.h | 4 +- .../cheryl/core/game-framework/game-runtime.h | 30 +- .../game-framework/simulation-scheduler.h | 8 +- .../cheryl/core/game-framework/tick-context.h | 6 +- projects/engine/include/cheryl/core/logging.h | 2 +- .../cheryl/core/logging/backend-guard.h | 67 +-- .../include/cheryl/core/logging/log-names.h | 14 +- .../engine/include/cheryl/core/logging/log.h | 19 +- .../include/cheryl/core/rendering/camera.h | 24 +- .../cheryl/core/rendering/clip-region.h | 8 +- .../cheryl/core/rendering/draw-packet.h | 6 +- .../core/rendering/presentation-surface.h | 4 +- .../include/cheryl/core/rendering/renderer.h | 4 +- .../resources/asset-management/asset-loader.h | 24 +- .../asset-management/file-registry.h | 18 +- .../resources/asset-management/font-mgr.h | 16 +- .../asset-management/manifest-loader.h | 14 +- .../resources/asset-management/material-mgr.h | 14 +- .../asset-management/shader-asset-mgr.h | 19 +- .../resources/asset-management/shader-mgr.h | 14 +- .../resources/asset-management/sprite-mgr.h | 16 +- .../resources/asset-management/texture-mgr.h | 16 +- .../resources/asset-management/tileset-mgr.h | 12 +- .../cheryl/core/resources/fileio/file-mgr.h | 8 +- .../core/resources/fileio/fonts-system.h | 10 +- .../cheryl/core/subsystems/event-bus.h | 43 +- .../cheryl/core/subsystems/event-system.h | 10 +- projects/engine/include/cheryl/export.h | 108 +++++ projects/engine/include/cheryl/math/anchor.h | 38 +- .../include/cheryl/templates/asset-mgr.h | 10 +- .../include/cheryl/text/font-collection.h | 10 +- projects/engine/include/cheryl/text/layout.h | 8 +- projects/engine/include/cheryl/text/utf8.h | 6 +- projects/engine/include/internals.h | 4 +- .../engine/include/internals/exceptions.h | 29 +- .../engine/src/assets/resources/pipeline.cpp | 2 + .../assets/resources/resource-provider.cpp | 7 + projects/engine/src/assets/types/2d/ffont.cpp | 19 + .../types/2d/font-stb-allocation-internal.h | 4 +- .../assets/types/2d/font-upload-internal.h | 4 +- .../engine/src/assets/types/2d/stbfont.cpp | 2 + .../src/core/controls/input-interface.cpp | 2 + .../src/core/engine/engine-context-internal.h | 4 +- .../src/core/engine/event-delivery-internal.h | 4 +- .../engine/src/core/engine/worker-affinity.h | 8 +- .../src/core/engine/worker-pool-internal.h | 6 +- .../core/engine/worker-topology-internal.h | 6 +- .../game-framework/game-runtime-internal.h | 4 +- projects/engine/src/core/logging/log.cpp | 32 ++ projects/engine/src/core/rendering/camera.cpp | 2 + .../asset-management/asset-loader.cpp | 7 + .../asset-management/file-registry.cpp | 7 + .../resources/asset-management/font-mgr.cpp | 7 + .../asset-management/material-mgr.cpp | 7 + .../asset-management/shader-asset-mgr.cpp | 7 + .../resources/asset-management/shader-mgr.cpp | 7 + .../resources/asset-management/sprite-mgr.cpp | 7 + .../asset-management/texture-mgr.cpp | 7 + .../asset-management/tileset-mgr.cpp | 7 + .../src/core/subsystems/event-system.cpp | 7 + projects/engine/src/internal/exceptions.cpp | 2 + projects/engine/src/text/font-collection.cpp | 7 +- .../support/debug-terminal/CMakeLists.txt | 5 +- .../cheryl/core/debug-terminal/startup.h | 4 +- .../support/logging-config/CMakeLists.txt | 4 +- .../support/signal-handlers/CMakeLists.txt | 13 +- .../support/signal-handlers/src/main.cpp | 7 +- .../engine/support/startup/CMakeLists.txt | 3 +- .../include/cheryl/core/engine/startup.h | 18 +- .../support/worker-topology/CMakeLists.txt | 6 +- .../support/worker-topology/cmake/Hwloc.cmake | 2 +- .../engine/worker-topology/configuration.h | 10 +- .../core/engine/worker-topology/hwloc.h | 4 +- projects/engine/tests/consumer/CMakeLists.txt | 5 + projects/engine/tests/consumer/src/main.cpp | 6 +- .../engine/tests/unit/src/font-selection.cpp | 21 +- projects/modules/README.md | 17 +- .../modules/audio/miniaudio/CMakeLists.txt | 10 +- projects/modules/audio/miniaudio/README.md | 6 +- .../include/cheryl/backends/miniaudio.h | 36 +- .../modules/graphics/opengl/CMakeLists.txt | 7 +- .../include/cheryl/backends/opengl/context.h | 4 +- .../cheryl/backends/opengl/glfw-backend.h | 6 +- .../cheryl/backends/opengl/glfw-context.h | 16 +- .../cheryl/backends/opengl/glslprogram.h | 28 +- .../include/cheryl/backends/opengl/pipeline.h | 20 +- .../include/cheryl/backends/opengl/renderer.h | 30 +- .../backends/opengl/resource-lifetime.h | 40 +- .../backends/opengl/resource-provider.h | 28 +- .../include/cheryl/backends/opengl/sampler.h | 10 +- .../include/cheryl/backends/opengl/texture.h | 14 +- .../backends/opengl/vertex-array-object.h | 20 +- .../opengl/src/backends/opengl/debug-output.h | 14 +- .../src/backends/opengl/program-builder.h | 6 +- .../src/backends/opengl/renderer-internal.h | 4 +- .../opengl/resource-lifetime-internal.h | 8 +- .../opengl/support/gl46/CMakeLists.txt | 3 +- .../opengl/support/startup/CMakeLists.txt | 3 +- .../include/cheryl/backends/opengl/startup.h | 6 +- .../opengl/tests/consumer/CMakeLists.txt | 5 + .../debug-terminal-linux/CMakeLists.txt | 18 +- .../platform/debug-terminal-linux/README.md | 33 +- .../cheryl/modules/debug-terminal-linux.h | 2 +- .../platform/native-glfw/CMakeLists.txt | 33 +- .../modules/platform/native-glfw/README.md | 2 +- .../compat/gainput-dll-exports.cpp | 6 + .../cheryl/backends/native-glfw/diagnostics.h | 4 +- .../cheryl/core/controls/glfw-bindings.h | 10 +- .../cheryl/core/controls/input-mapper.h | 12 +- .../cheryl/core/controls/input-system.h | 36 +- .../cheryl/core/display/display-system.h | 26 +- .../include/cheryl/core/display/window.h | 24 +- .../src/core/controls/input-system.cpp | 7 + .../src/core/display/glfw-diagnostics.cpp | 13 + .../src/core/display/glfw-diagnostics.h | 14 +- .../native-glfw/tests/consumer/CMakeLists.txt | 5 + .../native-glfw/tests/consumer/src/main.cpp | 9 + projects/modules/ui/rmlui/CMakeLists.txt | 19 +- projects/modules/ui/rmlui/README.md | 31 +- projects/modules/ui/rmlui/cmake/RmlUi.cmake | 5 +- .../modules/ui/rmlui/cmake/TestFont.cmake | 3 + .../ui/rmlui/include/cheryl/ui/rmlui/input.h | 6 +- .../rmlui/include/cheryl/ui/rmlui/rendering.h | 62 +-- .../ui/rmlui/include/cheryl/ui/rmlui/scene.h | 12 +- .../rmlui/include/cheryl/ui/rmlui/session.h | 34 +- projects/modules/ui/tgui/CMakeLists.txt | 8 +- projects/modules/ui/tgui/cmake/TGUI.cmake | 4 + .../ui/tgui/include/cheryl/ui/tgui/input.h | 4 +- .../tgui/include/cheryl/ui/tgui/rendering.h | 46 +- .../ui/tgui/include/cheryl/ui/tgui/scene.h | 12 +- .../ui/tgui/include/cheryl/ui/tgui/session.h | 32 +- 226 files changed, 3759 insertions(+), 1168 deletions(-) create mode 100644 .github/actions/migrate-compiler-cache/action.yml create mode 100644 .github/ci/sdk-consumer/state-bridge.cpp create mode 100644 .github/ci/sdk-consumer/state-bridge.h create mode 100644 .github/ci/sdk-consumer/state.cpp create mode 100644 cmake/CherylLibraries.cmake create mode 100644 docs/generation/static/appearance.js create mode 100644 docs/generation/static/layout.css create mode 100644 docs/generation/static/theme.css create mode 100644 projects/engine/include/cheryl/export.h create mode 100644 projects/modules/platform/native-glfw/compat/gainput-dll-exports.cpp diff --git a/.github/actions/migrate-compiler-cache/action.yml b/.github/actions/migrate-compiler-cache/action.yml new file mode 100644 index 00000000..042fa5ae --- /dev/null +++ b/.github/actions/migrate-compiler-cache/action.yml @@ -0,0 +1,26 @@ +name: Import a previous compiler cache +description: Stage one compatible per-job compiler snapshot for a cold family cache. +inputs: + legacy-prefix: + description: Previous per-job cache prefix without its branch-layer suffix. + required: true +runs: + using: composite + steps: + - name: Describe previous compiler cache + id: migration + shell: bash + env: + LEGACY_CACHE_PREFIX: ${{ inputs.legacy-prefix }} + run: python .github/ci/compiler_cache.py migration-plan --prefix "$LEGACY_CACHE_PREFIX" + - name: Restore previous compiler cache + id: legacy + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.migration.outputs.path }} + key: ${{ steps.migration.outputs.key }} + restore-keys: ${{ steps.migration.outputs.prefix }} + - name: Stage previous compiler cache + if: steps.legacy.outputs.cache-matched-key != '' + shell: bash + run: python .github/ci/compiler_cache.py import diff --git a/.github/actions/restore-compiler-cache/action.yml b/.github/actions/restore-compiler-cache/action.yml index 8d7bbc6f..cd66ffe9 100644 --- a/.github/actions/restore-compiler-cache/action.yml +++ b/.github/actions/restore-compiler-cache/action.yml @@ -1,16 +1,25 @@ name: Restore layered compiler caches -description: Restore main, develop and current compiler layers and configure ccache. +description: Restore immutable compiler layers and optionally attach a fresh writable delta. inputs: prefix: - description: Compiler cache format, runner, matrix profile and configuration prefix. + description: Compiler cache format, runner, architecture and compiler family prefix. required: true + attach: + description: Configure ccache for a worker; publishers only restore the layers. + default: 'true' outputs: current-path: - description: Writable layer to save after compilation. + description: Current immutable layer, or the publisher's merged layer to save. value: ${{ steps.metadata.outputs.current-path }} current-key: - description: New immutable key for the writable layer. + description: New immutable key for this run's family snapshot. value: ${{ steps.metadata.outputs.current-key }} + delta-archive: + description: Worker archive path for this run's compiler additions. + value: ${{ steps.metadata.outputs.delta-archive }} + current-hit: + description: Whether the current family layer was restored. + value: ${{ steps.current.outputs.cache-matched-key != '' }} runs: using: composite steps: @@ -19,7 +28,7 @@ runs: shell: bash env: CACHE_PREFIX: ${{ inputs.prefix }} - run: python .github/ci/compiler_cache.py metadata --prefix "$CACHE_PREFIX" + run: python .github/ci/compiler_cache.py plan --prefix "$CACHE_PREFIX" - name: Restore main compiler baseline if: steps.metadata.outputs.restore-main == 'true' uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -35,11 +44,13 @@ runs: key: ${{ steps.metadata.outputs.develop-prefix }}${{ github.run_id }}-${{ github.run_attempt }} restore-keys: ${{ steps.metadata.outputs.develop-prefix }} - name: Restore current compiler additions + id: current uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.metadata.outputs.current-path }} key: ${{ steps.metadata.outputs.current-key }} restore-keys: ${{ steps.metadata.outputs.current-prefix }} - name: Configure compiler cache layers + if: inputs.attach == 'true' shell: bash - run: python .github/ci/compiler_cache.py configure + run: python .github/ci/compiler_cache.py attach diff --git a/.github/ci/cache_cleanup.py b/.github/ci/cache_cleanup.py index c59f9bb4..59885d7b 100644 --- a/.github/ci/cache_cleanup.py +++ b/.github/ci/cache_cleanup.py @@ -15,13 +15,31 @@ GIB = 1024 ** 3 SNAPSHOT = re.compile( - r"^((?:ccache-v[12]|dependencies-v1|pip-v1)-.+|build-deps-v2-.+-binaries-[0-9a-f]+)-[0-9]+-[0-9]+$" + r"^((?:ccache-v[123]|dependencies-v1|pip-v1)-.+|build-deps-v2-.+-binaries-[0-9a-f]+)-[0-9]+-[0-9]+$" ) PULL_REF = re.compile(r"^refs/pull/([1-9][0-9]*)/merge$") -KNOWN_PREFIXES = ("ccache-v1-", "ccache-v2-", "dependencies-v1-", "pip-v1-", "docs-tools-v1-", +KNOWN_PREFIXES = ("ccache-v1-", "ccache-v2-", "ccache-v3-", "dependencies-v1-", "pip-v1-", "docs-tools-v1-", "windows-tools-v1-", "submodule-git-v1-", "apt-debs-v1-", "build-deps-v2-") ENVIRONMENT_KEY = re.compile(r"actions-env-cache-v[1-9][0-9]*-.+") SOURCE_KEY = re.compile(r"(build-deps-v2-.+-(?:main|develop)-sources-)[0-9a-f]{64}") +COMPILER_POOL = re.compile( + r"(ccache-v3-(?:Linux|Windows)-[A-Za-z0-9_.]+-(?:linux-gcc14|linux-clang18|windows-msvc))-(?:main|develop|pr|branch)" +) +LEGACY_COMPILER = re.compile( + r"ccache-v([12])-(ubuntu-24\.04|windows-2025)-([A-Za-z0-9_.]+)-(.+)-(Release|Debug|RelWithDebInfo)" + r"(?:-(main|develop|pr|branch))?" +) +# Only the pinned matrix profiles have a known replacement pool. +LEGACY_FAMILIES = { + ("ubuntu-24.04", "linux-gcc-hwloc", "Release"): ("Linux", "linux-gcc14"), + ("ubuntu-24.04", "linux-gcc-neutral", "Release"): ("Linux", "linux-gcc14"), + ("ubuntu-24.04", "linux-modules", "Release"): ("Linux", "linux-gcc14"), + ("ubuntu-24.04", "linux-modules-shared", "Release"): ("Linux", "linux-gcc14"), + ("ubuntu-24.04", "linux-clang-debug", "Debug"): ("Linux", "linux-clang18"), + ("windows-2025", "windows-msvc-hwloc", "RelWithDebInfo"): ("Windows", "windows-msvc"), + ("windows-2025", "windows-modules", "RelWithDebInfo"): ("Windows", "windows-msvc"), + ("windows-2025", "windows-modules-shared", "RelWithDebInfo"): ("Windows", "windows-msvc"), +} MAINTENANCE_PATH = ".github/workflows/cache-maintenance.yml" @@ -30,6 +48,22 @@ def lineage(key): return match.group(1) if match else None +def compiler_pool(logical): + if logical is None: + return None + pooled = COMPILER_POOL.fullmatch(logical) + if pooled: + return pooled.group(1) + legacy = LEGACY_COMPILER.fullmatch(logical) + if not legacy: + return None + revision, runner, architecture, profile, configuration, role = legacy.groups() + if (revision == "2") != bool(role): + return None + family = LEGACY_FAMILIES.get((runner, profile, configuration)) + return f"ccache-v3-{family[0]}-{architecture}-{family[1]}" if family else None + + def timestamp(value): if not isinstance(value, str): raise ValueError("Cache timestamps must be ISO 8601 strings") @@ -101,6 +135,11 @@ def remove(cache, reason): if logical is not None and logical.startswith("ccache-v2-") and logical.rsplit("-", 1)[1] in {"main", "develop", "pr", "branch"} } + pooled_profiles = { + (cache["ref"], pool) + for cache, _, _, logical in entries + if cache["key"].startswith("ccache-v3-") and (pool := compiler_pool(logical)) is not None + } newest = {} baseline = {} baseline_policy = {} @@ -112,12 +151,17 @@ def remove(cache, reason): if bucket not in newest or (created, cache["id"]) > (newest[bucket][1], newest[bucket][0]["id"]): newest[bucket] = record priority, reason = 0, "least recently used" - replaced = cache["key"].startswith("ccache-v1-") and (cache["ref"], logical) in layered_profiles + pooled_replacement = (cache["key"].startswith(("ccache-v1-", "ccache-v2-")) + and (cache["ref"], compiler_pool(logical)) in pooled_profiles) + if pooled_replacement: + remove(cache, "replaced compiler snapshot") + replaced = pooled_replacement or (cache["key"].startswith("ccache-v1-") + and (cache["ref"], logical) in layered_profiles) if not replaced: known = cache["key"].startswith(KNOWN_PREFIXES) or ENVIRONMENT_KEY.fullmatch(cache["key"]) if cache["ref"] == "refs/heads/" + default_branch and known: priority, reason = 2, "default-branch baseline" - if source or (logical is not None and cache["key"].startswith(("ccache-v1-", "ccache-v2-", "build-deps-v2-"))): + if source or (logical is not None and cache["key"].startswith(("ccache-v1-", "ccache-v2-", "ccache-v3-", "build-deps-v2-"))): match = PULL_REF.fullmatch(cache["ref"]) if cache["ref"] == "refs/heads/main": priority, reason = 3, "main baseline" diff --git a/.github/ci/compiler_cache.py b/.github/ci/compiler_cache.py index b1441dc0..d2192cbe 100644 --- a/.github/ci/compiler_cache.py +++ b/.github/ci/compiler_cache.py @@ -1,23 +1,35 @@ -"""Configure ordered native ccache layers and save only current-layer additions.""" +"""Share native ccache layers across compatible jobs and publish their union.""" import argparse import hashlib import json +import math import os from pathlib import Path, PurePath import re +import shutil import stat -import subprocess +import tarfile +import tempfile ROOT = Path(__file__).resolve().parents[2] -PREFIX = re.compile(r"ccache-v2-[A-Za-z0-9][A-Za-z0-9_.-]*") +PREFIX = re.compile(r"ccache-v3-(?:Linux-[A-Za-z0-9_]+-linux-(?:gcc14|clang18)|Windows-[A-Za-z0-9_]+-windows-msvc)") +LEGACY_PREFIXES = { + "linux-gcc14": ("ubuntu-24.04", ("linux-gcc-hwloc-Release", "linux-gcc-neutral-Release", + "linux-modules-Release", "linux-modules-shared-Release")), + "linux-clang18": ("ubuntu-24.04", ("linux-clang-debug-Debug",)), + "windows-msvc": ("windows-2025", ("windows-msvc-hwloc-RelWithDebInfo", + "windows-modules-RelWithDebInfo", "windows-modules-shared-RelWithDebInfo")), +} PULL_REF = re.compile(r"refs/pull/([1-9][0-9]*)/merge") # Native file-backend keys and CacheEntry v1 result headers in ccache 4.9/4.14. NATIVE_KEY = re.compile(r"[0-9a-f]{2}/[0-9a-f]{38}") RESULT_HEADER = b"\xcc\xac\x01\x00" EMPTY_MARKER = ".cheryl-empty-layer" METADATA_FILES = {EMPTY_MARKER, "CACHEDIR.TAG"} +LAYER_LIMIT = 1024 ** 3 +ARCHIVE_ENTRY_LIMIT = 200_000 def _valid_ref(ref): @@ -62,7 +74,7 @@ def _context(environment, event): def metadata(prefix, *, environment=None, event=None, root=ROOT): environment, event = _context(environment, event) if not isinstance(prefix, str) or not PREFIX.fullmatch(prefix): - raise ValueError("Compiler cache prefix must start with ccache-v2- and contain only letters, numbers, dots, underscores and hyphens") + raise ValueError("Compiler cache prefix must identify a supported ccache-v3 runner, architecture and compiler family") for name in ("GITHUB_RUN_ID", "GITHUB_RUN_ATTEMPT"): value = environment.get(name) if not isinstance(value, str) or not re.fullmatch(r"[1-9][0-9]*", value): @@ -72,6 +84,8 @@ def metadata(prefix, *, environment=None, event=None, root=ROOT): role = selected["role"] output = {"role": role, "main-path": str(directory / "main"), "develop-path": str(directory / "develop"), "current-path": str(directory / role), + "delta-path": str(directory / "delta"), + "delta-archive": str(directory.parent / "compiler-artifacts" / "delta.tar"), "main-prefix": f"{prefix}-main-", "develop-prefix": f"{prefix}-develop-", "current-prefix": f"{prefix}-{role}-"} output["current-key"] = output["current-prefix"] + environment["GITHUB_RUN_ID"] + "-" + environment["GITHUB_RUN_ATTEMPT"] @@ -167,41 +181,177 @@ def _layers(environment, event, root): def configure(*, environment=None, event=None, root=ROOT): environment, event = _context(environment, event) info, current, parents = _layers(environment, event, root) - for directory in [current, *parents]: + delta = Path(info["delta-path"]) + for directory in [delta, current, *parents]: _files(directory) directory.mkdir(parents=True, exist_ok=True) + if _entries(delta): + raise ValueError("A compiler worker must start with a fresh writable delta") stats = Path(root).absolute() / "build" / "ccache" _files(stats) stats.mkdir(parents=True, exist_ok=True) - state = {"key": info["current-key"], "path": str(current), "before": fingerprint(current), - "entries": len(_entries(current))} - (Path(root) / "build" / "cache" / "compiler-state.json").write_text(json.dumps(state), encoding="utf-8") return {"CCACHE_DIR": str(stats), "CCACHE_REMOTE_ONLY": "1", - "CCACHE_REMOTE_STORAGE": storage_configuration(current, parents), + "CCACHE_REMOTE_STORAGE": storage_configuration(delta, [current, *parents]), "CCACHE_NOFILECLONE": "1", "CCACHE_NOHARDLINK": "1"} -def prepare_save(*, environment=None, event=None, root=ROOT): +def _native_entries(directory): + entries = _entries(directory) + if any(not NATIVE_KEY.fullmatch(name) for name in entries): + raise ValueError("Compiler archives must contain only native file-backend cache entries") + return entries + + +def trim(directory, *, limit=LAYER_LIMIT): + entries = _native_entries(directory) + sizes = {name: path.stat().st_size for name, path in entries.items()} + total = sum(sizes.values()) + removed = 0 + for name, path in sorted(entries.items(), key=lambda item: (item[1].stat().st_mtime_ns, item[0])): + if total <= limit: + break + path.unlink() + total -= sizes[name] + removed += 1 + return removed + + +def merge(destination, source): + """Union results by native key; keep one complete, newest manifest per key.""" + destination_files = _native_entries(destination) + source_files = _native_entries(source) + for name, path in sorted(source_files.items()): + target = destination_files.get(name) + if target is not None: + existing, incoming = target.stat(), path.stat() + if _is_result(target) and _is_result(path): + # The same result key can have different serialization timestamps. + if incoming.st_mtime_ns > existing.st_mtime_ns: + os.utime(target, ns=(existing.st_atime_ns, incoming.st_mtime_ns)) + continue + if incoming.st_mtime_ns < existing.st_mtime_ns: + continue + target = Path(destination) / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target) + + +def pack(output, *, environment=None, event=None, root=ROOT): environment, event = _context(environment, event) info, current, parents = _layers(environment, event, root) - state = json.loads((Path(root) / "build" / "cache" / "compiler-state.json").read_text(encoding="utf-8")) - if not isinstance(state, dict) or state.get("key") != info["current-key"] or state.get("path") != str(current): - raise ValueError("Compiler cache snapshot does not match this run and layer") - if not isinstance(state.get("before"), str) or not re.fullmatch(r"[a-f0-9]{64}", state["before"]) or type(state.get("entries")) is not int or state["entries"] < 0: - raise ValueError("Compiler cache snapshot does not match this run and layer") + delta = Path(info["delta-path"]) + _native_entries(delta) + removed = deduplicate(delta, [current, *parents]) + trimmed = trim(delta) + entries = _native_entries(delta) + output = Path(output).absolute() + if output != Path(info["delta-archive"]): + raise ValueError("Compiler delta archive must use the planned output path") + _files(output.parent) + output.parent.mkdir(parents=True, exist_ok=True) + output.unlink(missing_ok=True) + if entries: + # Cache entries are already compressed; tar preserves their LRU timestamps. + with tarfile.open(output, "w", format=tarfile.PAX_FORMAT) as archive: + for name, path in sorted(entries.items()): + member = tarfile.TarInfo(name) + status = path.stat() + member.size = status.st_size + member.mtime = status.st_mtime + member.mode = 0o644 + with path.open("rb") as source: + archive.addfile(member, source) + return {"has-content": str(bool(entries)).lower(), "removed": str(removed), "trimmed": str(trimmed)} + + +def _archive_members(archive): + members = [] + seen = set() + total = 0 + for member in archive: + if (not member.isfile() or not NATIVE_KEY.fullmatch(member.name) or member.name in seen + or member.size < 0 or not math.isfinite(member.mtime) or member.mtime < 0): + raise ValueError("Compiler delta archive contains an invalid native entry") + total += member.size + if total > LAYER_LIMIT or len(members) >= ARCHIVE_ENTRY_LIMIT: + raise ValueError("Compiler delta archive exceeds the layer limits") + seen.add(member.name) + members.append(member) + return members + + +def _merge_archives(current, directory): + archives = sorted(_files(directory).values()) + # Validate every contribution before changing the restored snapshot. + for path in archives: + if path.name != "delta.tar": + raise ValueError("Expected a compiler delta.tar artifact") + with tarfile.open(path, "r:") as archive: + _archive_members(archive) + for path in archives: + with tarfile.open(path, "r:") as archive: + members = _archive_members(archive) + with tempfile.TemporaryDirectory(prefix="compiler-delta-", dir=current.parent) as temporary: + incoming = Path(temporary) + for member in members: + target = incoming / member.name + target.parent.mkdir(parents=True, exist_ok=True) + with archive.extractfile(member) as source, target.open("wb") as output: + shutil.copyfileobj(source, output) + os.utime(target, (member.mtime, member.mtime)) + merge(current, incoming) + return len(archives) + + +def migration_plan(prefix, *, environment=None, event=None, root=ROOT): + environment, event = _context(environment, event) + info, current, _ = _layers(environment, event, root) + selected = environment["CHERYL_CCACHE_PREFIX"] + family = next(family for family in LEGACY_PREFIXES if selected.endswith("-" + family)) + runner, profiles = LEGACY_PREFIXES[family] + architecture = selected.split("-")[3] + allowed = {f"ccache-v2-{runner}-{architecture}-{profile}" for profile in profiles} + if prefix not in allowed: + raise ValueError("Legacy compiler cache must belong to the selected runner and compiler family") + legacy_prefix = f"{prefix}-{info['role']}-" + return {"path": str(current), "prefix": legacy_prefix, + "key": legacy_prefix + environment["GITHUB_RUN_ID"] + "-" + environment["GITHUB_RUN_ATTEMPT"]} + + +def import_legacy(*, environment=None, event=None, root=ROOT): + environment, event = _context(environment, event) + _, current, _ = _layers(environment, event, root) + staging = current.parent / "migration" + merge(staging, current) + entries = _native_entries(current) + for path in entries.values(): + path.unlink() + for name in METADATA_FILES: + (current / name).unlink(missing_ok=True) + return {"imported": str(len(entries))} + + +def publish(deltas, *, environment=None, event=None, root=ROOT): + environment, event = _context(environment, event) + _, current, parents = _layers(environment, event, root) + _native_entries(current) + current.mkdir(parents=True, exist_ok=True) + before = fingerprint(current) + initial_snapshot = bool(_native_entries(current)) or (current / EMPTY_MARKER).exists() + merge(current, current.parent / "migration") + archives = _merge_archives(current, deltas) removed = deduplicate(current, parents) - subprocess.run(["ccache", "--trim-dir", str(current), "--trim-max-size", "1GiB", - "--trim-method", "mtime"], check=True) - after = fingerprint(current) - entries = _entries(current) + trimmed = trim(current) + entries = _native_entries(current) marker = current / EMPTY_MARKER if entries: marker.unlink(missing_ok=True) - elif state["entries"]: - # Supersede a restored nonempty archive even when all entries moved to a parent. + else: + # Remember an initialized empty layer so legacy imports are not repeated. marker.write_text("Current compiler cache layer is empty.\n", encoding="utf-8") - changed = after != state["before"] - return {"changed": str(changed).lower(), "removed": str(removed)} + return {"changed": str(fingerprint(current) != before or not initial_snapshot).lower(), + "has-content": str(bool(entries) or marker.exists()).lower(), + "removed": str(removed), "trimmed": str(trimmed), "archives": str(archives)} def _emit(values, destination): @@ -216,20 +366,33 @@ def _emit(values, destination): def main(): parser = argparse.ArgumentParser(description=__doc__) commands = parser.add_subparsers(dest="command", required=True) - describe = commands.add_parser("metadata") + describe = commands.add_parser("plan") describe.add_argument("--prefix", required=True) - commands.add_parser("configure") - commands.add_parser("prepare-save") + commands.add_parser("attach") + package = commands.add_parser("pack") + package.add_argument("--output", required=True) + publication = commands.add_parser("publish") + publication.add_argument("--deltas", required=True) + migration = commands.add_parser("migration-plan") + migration.add_argument("--prefix", required=True) + commands.add_parser("import") args = parser.parse_args() - if args.command == "metadata": + if args.command == "plan": _emit(metadata(args.prefix), "GITHUB_OUTPUT") _emit({"CHERYL_CCACHE_PREFIX": args.prefix}, "GITHUB_ENV") - elif args.command == "configure": + elif args.command == "attach": _emit(configure(), "GITHUB_ENV") else: - result = prepare_save() + if args.command == "pack": + result = pack(args.output) + elif args.command == "publish": + result = publish(args.deltas) + elif args.command == "migration-plan": + result = migration_plan(args.prefix) + else: + result = import_legacy() _emit(result, "GITHUB_OUTPUT") - print(f"Compiler cache additions: changed={result['changed']}, identical parent entries removed={result['removed']}") + print("Compiler cache " + args.command + ": " + json.dumps(result, sort_keys=True)) if __name__ == "__main__": diff --git a/.github/ci/package.py b/.github/ci/package.py index 562e6985..7e415093 100644 --- a/.github/ci/package.py +++ b/.github/ci/package.py @@ -4,7 +4,7 @@ import gzip import hashlib import json -from pathlib import Path +from pathlib import Path, PurePosixPath import platform import shutil import subprocess @@ -72,16 +72,43 @@ def copy_native_dependencies(prefix, stage): target.write_text(contents, encoding="utf-8") -def check_contents(stage): +def check_symlinks(stage, *, allow_symlinks=True): + root = stage.resolve() for path in stage.rglob("*"): if path.is_symlink(): - raise ValueError(f"SDK files must be self-contained: {path}") + target = path.readlink() + if not allow_symlinks or target.is_absolute(): + raise ValueError(f"SDK symlinks must be relative and supported by the archive: {path}") + try: + resolved = path.resolve(strict=True) + except (OSError, RuntimeError) as error: + raise ValueError(f"SDK symlink is broken or cyclic: {path}") from error + if not resolved.is_relative_to(root): + raise ValueError(f"SDK symlink escapes the SDK prefix: {path}") + + +def check_contents(stage, metadata=None): + check_symlinks(stage) + metadata = json.loads((stage / "SDK-MANIFEST.json").read_text(encoding="utf-8")) if metadata is None else metadata + if (not isinstance(metadata, dict) or metadata.get("linkage") not in {"static", "shared"} + or not isinstance(metadata.get("component_versions"), dict) or not metadata["component_versions"].get("engine")): + raise ValueError("SDK metadata must declare linkage and the Engine component version") + for path in stage.rglob("*"): if path.suffix.lower() in (".c", ".cc", ".cpp", ".cxx", ".m", ".mm", ".exe"): raise ValueError(f"SDK must not include application or implementation files: {path}") if not (stage / "include/cheryl/core.h").is_file(): raise ValueError("The SDK is missing public Engine headers") - if not any((stage / "lib").glob("*cheryl-engine.*")): - raise ValueError("The SDK is missing the compiled Engine library") + artifacts = ["engine_library"] + if metadata["linkage"] == "shared" and metadata.get("system") == "Windows": + artifacts.append("engine_import_library") + for field in artifacts: + name = metadata.get(field) + path = PurePosixPath(name) if isinstance(name, str) else None + if not path or path.is_absolute() or ".." in path.parts or ":" in name or "\\" in name or not path.parts: + raise ValueError("SDK metadata needs a relative compiled Engine library path") + artifact = stage / path + if not artifact.is_file() or not artifact.resolve().is_relative_to(stage.resolve()): + raise ValueError(f"The SDK is missing the compiled Engine library: {name}") if not (stage / "lib/cmake/Cheryl/CherylConfig.cmake").is_file(): raise ValueError("The SDK is missing CMake integration") if not (stage / "share/licenses/cheryl/LICENSE").is_file(): @@ -89,6 +116,7 @@ def check_contents(stage): def archive_sdk(stage, destination): + check_symlinks(stage, allow_symlinks=destination.suffix != ".zip") if destination.suffix == ".zip": with zipfile.ZipFile(destination, "w", compression=zipfile.ZIP_DEFLATED) as archive: for path in sorted(stage.rglob("*")): @@ -100,7 +128,7 @@ def archive_sdk(stage, destination): archive.writestr(info, path.read_bytes()) else: with destination.open("wb") as raw, gzip.GzipFile(fileobj=raw, mode="wb", filename="", mtime=0) as compressed: - with tarfile.open(fileobj=compressed, mode="w") as archive: + with tarfile.open(fileobj=compressed, mode="w", dereference=False) as archive: for path in [stage, *sorted(stage.rglob("*"))]: info = archive.gettarinfo(str(path), arcname=path.relative_to(stage.parent).as_posix()) info.uid = info.gid = info.mtime = 0 @@ -126,7 +154,7 @@ def bundle(source, build, output, native_prefix=None): if "ID" in release: platform_name += "-" + release["ID"] + release.get("VERSION_ID", "") name = (f"cheryl-{metadata['version']}-sdk-{platform_name}-{architecture}-" - f"{metadata['compiler'].lower()}{metadata['compiler_version']}-{metadata['configuration'].lower()}-" + f"{metadata['compiler'].lower()}{metadata['compiler_version']}-{metadata['configuration'].lower()}-{metadata['linkage']}-" f"{metadata['revision'][:12]}") output.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(dir=output) as temporary: @@ -141,7 +169,13 @@ def bundle(source, build, output, native_prefix=None): metadata["system_dependencies"] = ["FreeType", "HarfBuzz", "ICU", "hwloc (when selected)", "libdw and its dependencies", "X11/OpenGL (when selected)", "libudev/libusb/Iconv (when required by packaged HIDAPI)"] - check_contents(stage) + check_contents(stage, metadata) + runtime_directories = [] + if metadata["system"] == "Windows": + runtime_directories = ["bin", "dependencies/bin"] + elif metadata["linkage"] == "shared": + runtime_directories = [PurePosixPath(metadata["engine_library"]).parent.as_posix()] + metadata["runtime_directories"] = [name for name in runtime_directories if (stage / name).is_dir()] text = json.dumps(metadata, indent=2, sort_keys=True) + "\n" (stage / "SDK-MANIFEST.json").write_text(text, encoding="utf-8") destination = output / (name + (".zip" if metadata["system"] == "Windows" else ".tar.gz")) diff --git a/.github/ci/run.py b/.github/ci/run.py index 02a64fe7..94e17a4e 100644 --- a/.github/ci/run.py +++ b/.github/ci/run.py @@ -1,4 +1,4 @@ -"""Configure, build and check one explicitly selected CI composition.""" +"""Build or check one explicitly selected CI composition.""" import argparse import os @@ -18,7 +18,9 @@ def main(arguments=None): parser.add_argument("--source", type=Path, default=Path(__file__).resolve().parents[2]) parser.add_argument("--build-dir", type=Path, required=True) parser.add_argument("--profile", choices=("headless", "modules", "consumer"), required=True) + parser.add_argument("--phase", choices=("all", "build", "test"), default="all") parser.add_argument("--build-type", choices=("Release", "Debug", "RelWithDebInfo"), default="Release") + parser.add_argument("--linkage", choices=("static", "shared"), default="static") parser.add_argument("--hwloc", choices=("ON", "OFF"), default="OFF") parser.add_argument("--jobs", type=int, default=os.cpu_count() or 1) parser.add_argument("--vcpkg-root", type=Path) @@ -31,6 +33,8 @@ def main(arguments=None): modules = args.profile == "modules" testing = args.profile != "consumer" environment = os.environ.copy() + if sys.platform == "win32" and args.linkage == "shared": + environment["PATH"] = str(build) + os.pathsep + environment.get("PATH", "") if testing: environment.setdefault("CHERYL_STB_ALLOCATION_TTF", str(source / "assets/fonts/DejaVuSans.ttf")) flag = lambda value: "ON" if value else "OFF" @@ -45,8 +49,10 @@ def main(arguments=None): f"-DCHERYL_BUILD_AUDIO_MINIAUDIO={flag(modules)}", f"-DCHERYL_BUILD_WORKER_TOPOLOGY_HWLOC={args.hwloc}", f"-DCHERYL_BUILD_WORKER_TOPOLOGY_JSON={flag(testing)}", "-DGAINPUT_ENABLE_HID=OFF", - "-DGLFW_BUILD_WAYLAND=OFF", "-Dgtest_force_shared_crt=ON", "-DBUILD_SHARED_LIBS=OFF", - "-DTGUI_SHARED_LIBS=OFF", f"-DCHERYL_BUILD_SDK={flag(args.sdk)}", "-DCMAKE_INSTALL_LIBDIR=lib"] + "-DGLFW_BUILD_WAYLAND=OFF", "-Dgtest_force_shared_crt=ON", + f"-DCHERYL_BUILD_SHARED_LIBS={flag(args.linkage == 'shared')}", + f"-DCHERYL_BUILD_SDK={flag(args.sdk)}", "-DCHERYL_BUILD_DOCS=OFF", + "-DCMAKE_INSTALL_LIBDIR=lib"] if modules and sys.platform == "win32": configure.append(f"-DPython_EXECUTABLE:FILEPATH={Path(sys.executable).resolve().as_posix()}") if sys.platform == "win32" and environment.get("CMAKE_CXX_COMPILER_LAUNCHER") == "ccache": @@ -76,8 +82,11 @@ def main(arguments=None): targets += module_suites + [f"consumer-module-{owner}" for owner in owners] + ["demo", "first-game"] suites += module_suites consumers += [f"cheryl-{owner}-consumer" for owner in owners] - run(configure, source, environment) - run(["cmake", "--build", build, "--target", *targets, "--parallel", str(args.jobs)], source, environment) + if args.phase != "test": + run(configure, source, environment) + run(["cmake", "--build", build, "--target", *targets, "--parallel", str(args.jobs)], source, environment) + if args.phase == "build": + return if testing: # Owner runners only: no aggregates or native desktop/device suites. pattern = "^(" + "|".join(name + r"\." for name in suites) + "|consumer-worker-topology$)" diff --git a/.github/ci/sdk-consumer/CMakeLists.txt b/.github/ci/sdk-consumer/CMakeLists.txt index 5fdeaf4e..35d3ede6 100644 --- a/.github/ci/sdk-consumer/CMakeLists.txt +++ b/.github/ci/sdk-consumer/CMakeLists.txt @@ -66,3 +66,22 @@ if(TARGET Cheryl::WorkerTopology::Json) target_compile_definitions(sdk-worker PRIVATE CHERYL_CONSUMER_WORKER_JSON=1) endif() add_test(NAME sdk-worker COMMAND sdk-worker) + +# Exercise Engine-owned state and exceptions across a separately compiled library. +get_target_property(sdk_engine_kind Cheryl::Engine TYPE) +set(sdk_bridge_kind STATIC) +if(sdk_engine_kind STREQUAL "SHARED_LIBRARY") + set(sdk_bridge_kind SHARED) +endif() +add_library(sdk-state-bridge ${sdk_bridge_kind} state-bridge.cpp) +target_link_libraries(sdk-state-bridge PRIVATE Cheryl::Engine) +target_compile_definitions(sdk-state-bridge PRIVATE SDK_STATE_BUILDING) +if(sdk_bridge_kind STREQUAL "STATIC") + target_compile_definitions(sdk-state-bridge PUBLIC SDK_STATE_STATIC) +endif() +set_target_properties(sdk-state-bridge PROPERTIES + CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN YES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") +add_executable(sdk-state state.cpp) +target_link_libraries(sdk-state PRIVATE sdk-state-bridge Cheryl::Engine) +add_test(NAME sdk-state COMMAND sdk-state) diff --git a/.github/ci/sdk-consumer/state-bridge.cpp b/.github/ci/sdk-consumer/state-bridge.cpp new file mode 100644 index 00000000..403d0e7c --- /dev/null +++ b/.github/ci/sdk-consumer/state-bridge.cpp @@ -0,0 +1,21 @@ +#include "state-bridge.h" + +#include +#include +#include + +const void* sdk_event_system() { + return &CE::Events::EventSystem::get(); +} + +const void* sdk_logging_mutex() { + return &CE::Logging::Detail::default_logger_mutex; +} + +bool sdk_provider_bound(const CE::Rendering::Resources::Provider& provider) { + return CE::Assets::Cache::Context::is_bound_to(provider); +} + +void sdk_throw_exception() { + throw CE::Exceptions::runtime_exception(CE_HERE, "SDK bridge exception"); +} diff --git a/.github/ci/sdk-consumer/state-bridge.h b/.github/ci/sdk-consumer/state-bridge.h new file mode 100644 index 00000000..33c81ff0 --- /dev/null +++ b/.github/ci/sdk-consumer/state-bridge.h @@ -0,0 +1,22 @@ +#pragma once + +#if defined(SDK_STATE_STATIC) +#define SDK_STATE_API +#elif defined(_WIN32) +#if defined(SDK_STATE_BUILDING) +#define SDK_STATE_API __declspec(dllexport) +#else +#define SDK_STATE_API __declspec(dllimport) +#endif +#else +#define SDK_STATE_API __attribute__((visibility("default"))) +#endif + +namespace CE::Rendering::Resources { + struct Provider; +} + +SDK_STATE_API const void* sdk_event_system(); +SDK_STATE_API const void* sdk_logging_mutex(); +SDK_STATE_API bool sdk_provider_bound(const CE::Rendering::Resources::Provider& provider); +SDK_STATE_API void sdk_throw_exception(); diff --git a/.github/ci/sdk-consumer/state.cpp b/.github/ci/sdk-consumer/state.cpp new file mode 100644 index 00000000..8803b511 --- /dev/null +++ b/.github/ci/sdk-consumer/state.cpp @@ -0,0 +1,43 @@ +#include "state-bridge.h" + +#include +#include +#include +#include + +#include +#include + +namespace { + struct MemoryProvider final : CE::Rendering::Resources::Provider { + std::shared_ptr create_image(const CE::Assets::Images::Decoded&) override { return {}; } + std::shared_ptr + create_font_atlas(std::span, CE::Rendering::Geometry::PixelSize) override { + return {}; + } + std::shared_ptr + upload_geometry(std::span, CE::Rendering::Geometry::Topology) override { + return {}; + } + std::shared_ptr link_program(const std::vector&) override { return {}; } + }; + + struct CacheBinding : CE::Assets::Cache::Context { + using Context::bind_provider; + }; +} + +int main() { + const bool singleton = sdk_event_system() == &CE::Events::EventSystem::get(); + const bool logging = sdk_logging_mutex() == &CE::Logging::Detail::default_logger_mutex; + MemoryProvider provider; + CacheBinding::bind_provider(provider); + const bool cache = sdk_provider_bound(provider); + bool exception = false; + try { + sdk_throw_exception(); + } catch (const CE::Exceptions::runtime_exception& error) { + exception = std::string_view{error.what()}.find("SDK bridge exception") != std::string_view::npos; + } + return singleton && logging && cache && exception ? 0 : 1; +} diff --git a/.github/ci/test_cache_cleanup.py b/.github/ci/test_cache_cleanup.py index a4db4733..7066237b 100644 --- a/.github/ci/test_cache_cleanup.py +++ b/.github/ci/test_cache_cleanup.py @@ -21,6 +21,7 @@ GIB = 1024 ** 3 COMPILER = "ccache-v1-ubuntu-24.04-X64-linux-gcc-hwloc-Release" LAYERED = COMPILER.replace("ccache-v1-", "ccache-v2-") +POOLED = "ccache-v3-Linux-X64-linux-gcc14" BUILD_DEPS = "build-deps-v2-Windows-X64-main-modules-binaries-" + "a" * 64 @@ -66,6 +67,8 @@ def test_lineages(self): for key, expected in ( (f"{COMPILER}-12345-2", COMPILER), (f"{LAYERED}-main-12345-2", f"{LAYERED}-main"), + (f"{POOLED}-main-12345-2", f"{POOLED}-main"), + (f"{POOLED}-pr-12345-2", f"{POOLED}-pr"), ("dependencies-v1-Windows-X64-modules-abcd-12345-2", "dependencies-v1-Windows-X64-modules-abcd"), ("pip-v1-Windows-X64-cpython-312-abcd-12345-2", @@ -197,11 +200,12 @@ def test_closed_only(self): def test_closed_families(self): keys = [f"{COMPILER}-1-1", f"{LAYERED}-pr-2-1", "dependencies-v1-Windows-X64-modules-hash-3-1", "pip-v1-Windows-X64-cpython-313-hash-4-1", f"{BUILD_DEPS}-5-1", - "build-deps-v2-Linux-X64-main-sources-hash", "actions-env-cache-v2-Linux-X64-docs-identity-tools"] + "build-deps-v2-Linux-X64-main-sources-hash", "actions-env-cache-v2-Linux-X64-docs-identity-tools", + f"{POOLED}-pr-8-1"] rows = [cache(index, key=key, ref="refs/pull/3/merge") for index, key in enumerate(keys, 1)] - rows.append(cache(8, ref="refs/pull/30/merge")) + rows.append(cache(len(keys) + 1, ref="refs/pull/30/merge")) result = plan(rows, closed_prs={3}, closed_pr=3) - self.assertEqual(removed(result), {index: "closed pull request" for index in range(1, 8)}) + self.assertEqual(removed(result), {index: "closed pull request" for index in range(1, len(keys) + 1)}) def test_unconfirmed(self): result = plan([cache(1, ref="refs/pull/3/merge")], closed_pr=3) @@ -290,6 +294,73 @@ def test_layered_compaction(self): cache(3, key=f"{LAYERED}-develop-3-1")] self.assertEqual(removed(plan(rows)), {1: "superseded snapshot"}) + def test_pool_compaction(self): + rows = [cache(1, key=f"{POOLED}-main-1-1", ref="refs/heads/main", age=18000), + cache(2, key=f"{POOLED}-main-2-1", ref="refs/heads/main", age=7200), + cache(3, key=POOLED.replace("gcc14", "clang18") + "-main-3-1", ref="refs/heads/main"), + cache(4, key=f"{POOLED}-develop-4-1"), + cache(5, key=f"{POOLED}-main-5-1", ref="refs/heads/main", version="other-path"), + cache(6, key=POOLED.replace("X64", "ARM64") + "-main-6-1", ref="refs/heads/main")] + self.assertEqual(removed(plan(rows)), {1: "superseded snapshot"}) + + def test_pool_tiers(self): + rows = [cache(1, key=f"{POOLED}-main-1-1", ref="refs/heads/main", size=3 * GIB, accessed=18000), + cache(2, key=f"{POOLED}-develop-2-1", size=3 * GIB, accessed=14400), + cache(3, key=f"{POOLED}-pr-3-1", ref="refs/pull/3/merge", size=3 * GIB, accessed=10800), + cache(4, key="ordinary", ref="refs/heads/feature", size=GIB)] + result = plan(rows, open_prs={3}, budget_bytes=2 * GIB) + self.assertEqual(removed(result), {4: "least recently used", 3: "open pull request baseline", + 2: "develop baseline", 1: "main baseline"}) + self.assertEqual([row["id"] for row in result["deletions"]], [4, 3, 2, 1]) + + def test_pool_migration(self): + for runner, profile, configuration, operating_system, family in ( + ("ubuntu-24.04", "linux-gcc-hwloc", "Release", "Linux", "linux-gcc14"), + ("ubuntu-24.04", "linux-gcc-neutral", "Release", "Linux", "linux-gcc14"), + ("ubuntu-24.04", "linux-modules", "Release", "Linux", "linux-gcc14"), + ("ubuntu-24.04", "linux-modules-shared", "Release", "Linux", "linux-gcc14"), + ("ubuntu-24.04", "linux-clang-debug", "Debug", "Linux", "linux-clang18"), + ("windows-2025", "windows-msvc-hwloc", "RelWithDebInfo", "Windows", "windows-msvc"), + ("windows-2025", "windows-modules", "RelWithDebInfo", "Windows", "windows-msvc"), + ("windows-2025", "windows-modules-shared", "RelWithDebInfo", "Windows", "windows-msvc"), + ): + with self.subTest(profile=profile): + legacy = f"{runner}-X64-{profile}-{configuration}" + pooled = f"ccache-v3-{operating_system}-X64-{family}" + rows = [cache(1, key=f"ccache-v1-{legacy}-1-1", ref="refs/heads/main"), + cache(2, key=f"ccache-v2-{legacy}-main-2-1", ref="refs/heads/main"), + cache(3, key=f"{pooled}-main-3-1", ref="refs/heads/main")] + self.assertEqual(removed(plan(rows)), {1: "replaced compiler snapshot", 2: "replaced compiler snapshot"}) + + def test_pool_migration_scope(self): + legacy = cache(1, key=f"{LAYERED}-main-1-1", ref="refs/heads/main") + for update in ({"ref": "refs/heads/develop"}, + {"key": POOLED.replace("gcc14", "clang18") + "-main-2-1"}, + {"key": POOLED.replace("X64", "ARM64") + "-main-2-1"}, + {"key": "ccache-v3-Windows-X64-windows-msvc-main-2-1"}): + with self.subTest(update=update): + pooled = {**cache(2, key=f"{POOLED}-main-2-1", ref="refs/heads/main"), **update} + self.assertEqual(removed(plan([legacy, pooled])), {}) + + def test_pool_migration_unknown(self): + keys = [LAYERED.replace("linux-gcc-hwloc", "custom-gcc") + "-main-1-1", + LAYERED.replace("ubuntu-24.04", "ubuntu-26.04") + "-main-2-1", + LAYERED.replace("Release", "Debug") + "-main-3-1", + f"{POOLED}-main-4-1"] + rows = [cache(index, key=key, ref="refs/heads/main") for index, key in enumerate(keys, 1)] + self.assertEqual(removed(plan(rows)), {}) + + def test_pool_migration_protection(self): + for update, options in (({}, {"active_refs": {"refs/heads/main"}}), + ({}, {"unresolved_refs": {"refs/heads/main"}}), + ({"age": 60}, {}), ({"accessed": 60}, {})): + with self.subTest(update=update, options=options): + rows = [cache(1, key=f"{LAYERED}-main-1-1", ref="refs/heads/main", **update), + cache(2, key=f"{POOLED}-main-2-1", ref="refs/heads/main")] + result = plan(rows, **options) + self.assertEqual(removed(result), {}) + self.assertIn(1, result["protected_ids"]) + def test_migration(self): rows = [cache(1, ref="refs/heads/main", size=5 * GIB), cache(2, key=f"{LAYERED}-main-2-1", ref="refs/heads/main", diff --git a/.github/ci/test_ci.py b/.github/ci/test_ci.py index c0060ae2..d2e924f0 100644 --- a/.github/ci/test_ci.py +++ b/.github/ci/test_ci.py @@ -2,6 +2,7 @@ from contextlib import redirect_stdout import importlib.util +import hashlib import io import json from pathlib import Path @@ -20,6 +21,10 @@ spec = importlib.util.spec_from_file_location("ci_package", Path(__file__).with_name("package.py")) package = importlib.util.module_from_spec(spec) spec.loader.exec_module(package) +spec = importlib.util.spec_from_file_location("ci_verify_sdk", Path(__file__).with_name("verify_sdk.py")) +verify_sdk = importlib.util.module_from_spec(spec) +with patch.dict("sys.modules", {"package": package}): + spec.loader.exec_module(verify_sdk) class RunnerTests(unittest.TestCase): @@ -32,10 +37,12 @@ def setUp(self): self.source.mkdir() self.build = self.root / "build" self.commands = [] + self.environments = [] def execute(self, command, source, environment): command = list(map(str, command)) self.commands.append(command) + self.environments.append(dict(environment)) self.assertEqual(source, self.source) if command[0] == "ctest": self.assertEqual(environment["CHERYL_STB_ALLOCATION_TTF"], str(self.source / "assets/fonts/DejaVuSans.ttf")) @@ -62,6 +69,7 @@ def test_headless(self): self.assertIsNone(pattern.search(name), name) self.assertIn("Completed 1 checks; 1 skipped.", output) self.assertIn("Not accepted (skipped): fixture", output) + self.assertIn("-DCHERYL_BUILD_SHARED_LIBS=OFF", self.commands[0]) self.assertFalse(any(command[0].endswith("demo") for command in self.commands)) def test_modules(self): @@ -83,6 +91,41 @@ def test_consumer(self): self.assertEqual(self.commands[1][self.commands[1].index("--target") + 1:self.commands[1].index("--parallel")], ["consumer-cengine", "consumer-headers-cheryl"]) + def test_build_phase(self): + self.invoke("--profile", "headless", "--phase", "build", "--hwloc", "ON") + self.assertEqual(len(self.commands), 2) + self.assertEqual(self.commands[0][:2], ["cmake", "-S"]) + self.assertEqual(self.commands[1][:2], ["cmake", "--build"]) + for target in ("consumer-cengine", "tests-engine", "acceptance-engine", "tests-worker-topology-composition"): + self.assertIn(target, self.commands[1]) + + def test_test_phase(self): + self.invoke("--profile", "modules", "--phase", "test", "--hwloc", "ON", "--sdk") + self.assertEqual(self.commands[0][0], "ctest") + self.assertFalse(any(command[0] == "cmake" for command in self.commands)) + pattern = re.compile(self.commands[0][-1]) + for name in ("acceptance-engine.runtime_adapter.close_before_update", + "tests-worker-topology-composition.workers.config", "tests-ui-coexist.case"): + self.assertIsNotNone(pattern.search(name), name) + self.assertEqual(len(self.commands), 7) + self.assertTrue(all(len(command) == 1 for command in self.commands[1:])) + + def test_consumer_phase(self): + self.invoke("--profile", "consumer", "--phase", "test") + self.assertEqual(len(self.commands), 1) + self.assertEqual(Path(self.commands[0][0]).stem, "cheryl-consumer") + + def test_shared(self): + self.invoke("--profile", "modules", "--linkage", "shared") + self.assertIn("-DCHERYL_BUILD_SHARED_LIBS=ON", self.commands[0]) + self.assertFalse(any(option.startswith(("-DBUILD_SHARED_LIBS=", "-DTGUI_SHARED_LIBS=")) + for option in self.commands[0])) + + def test_windows_shared(self): + with patch.object(ci.sys, "platform", "win32"): + self.invoke("--profile", "consumer", "--phase", "test", "--linkage", "shared") + self.assertIn(str(self.build), self.environments[-1]["PATH"].split(ci.os.pathsep)) + def test_windows(self): with patch.object(ci.sys, "platform", "win32"): self.invoke("--profile", "headless", "--build-type", "RelWithDebInfo", "--hwloc", "ON", @@ -104,17 +147,67 @@ def fail_build(command, source, environment): self.assertEqual(len(calls), 2) +class SdkPhaseTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name).resolve() + self.build = self.root / "consumer" + self.packages = self.root / "packages" + self.packages.mkdir() + self.metadata = {"system": "Windows", "configuration": "RelWithDebInfo"} + archive = self.packages / "sdk.zip" + with zipfile.ZipFile(archive, "w") as output: + output.writestr("sdk/SDK-MANIFEST.json", json.dumps(self.metadata)) + digest = hashlib.sha256(archive.read_bytes()).hexdigest() + (self.packages / "SHA256SUMS").write_text(f"{digest} {archive.name}", encoding="utf-8") + + def invoke(self, phase=None): + arguments = ["verify_sdk.py", "--source", str(self.root), "--packages", str(self.packages), + "--build-dir", str(self.build), "--producer-build", str(self.root / "producer"), "--jobs", "2"] + if phase: + arguments += ["--phase", phase] + with patch("sys.argv", arguments), patch.object(verify_sdk, "check_contents"), \ + patch.object(verify_sdk.subprocess, "run") as command, redirect_stdout(io.StringIO()): + verify_sdk.main() + return command.call_args_list + + def test_split(self): + commands = self.invoke("build") + self.assertEqual([call.args[0][:2] for call in commands], [["cmake", "-S"], ["cmake", "--build"]]) + # Testing uses the already extracted SDK and never opens the package again. + (self.packages / "sdk.zip").unlink() + (self.packages / "SHA256SUMS").unlink() + commands = self.invoke("test") + self.assertEqual(len(commands), 1) + self.assertEqual(commands[0].args[0][0], "ctest") + sdk = self.build.parent / "consumer-relocated-sdk/sdk" + search = commands[0].kwargs["env"]["PATH"].split(verify_sdk.os.pathsep) + self.assertEqual(search[:2], [str(sdk / "bin"), str(sdk / "dependencies/bin")]) + + def test_combined(self): + commands = self.invoke() + self.assertEqual([call.args[0][0] for call in commands], ["cmake", "cmake", "ctest"]) + + def test_missing_build(self): + with self.assertRaises(FileNotFoundError): + self.invoke("test") + + class PackageTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory() self.addCleanup(self.temporary.cleanup) self.root = Path(self.temporary.name) self.stage = self.root / "sdk" - for name in ("include/cheryl/core.h", "lib/libcheryl-engine.a", "lib/cmake/Cheryl/CherylConfig.cmake", + self.metadata = {"version": "4.0.0", "component_versions": {"engine": "4.0.0"}, "linkage": "static", + "engine_library": "lib/libcheryl-engine-4.0.0.a", "engine_import_library": "", "system": "Linux"} + for name in ("include/cheryl/core.h", self.metadata["engine_library"], "lib/cmake/Cheryl/CherylConfig.cmake", "share/licenses/cheryl/LICENSE", "share/licenses/stb/LICENSE"): path = self.stage / name path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(name.encode()) + (self.stage / "SDK-MANIFEST.json").write_text(json.dumps(self.metadata), encoding="utf-8") def test_archive(self): package.check_contents(self.stage) @@ -141,10 +234,75 @@ def test_contents(self): with self.assertRaisesRegex(ValueError, "application or implementation"): package.check_contents(self.stage) path.unlink() - (self.stage / "lib/libcheryl-engine.a").unlink() + (self.stage / self.metadata["engine_library"]).unlink() + (self.stage / "lib/libcheryl-engine-other.a").write_bytes(b"unrelated library") with self.assertRaisesRegex(ValueError, "compiled Engine"): package.check_contents(self.stage) + def test_shared_artifacts(self): + for system, library, import_library in ( + ("Linux", "lib/libcheryl-engine.so.4.0.0", ""), + ("Windows", "bin/cheryl-engine-4.0.0.dll", "lib/cheryl-engine-4.0.0.lib"), + ): + with self.subTest(system=system): + metadata = {**self.metadata, "system": system, "linkage": "shared", + "engine_library": library, "engine_import_library": import_library} + for name in (library, import_library): + if name: + path = self.stage / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"compiled artifact") + package.check_contents(self.stage, metadata) + (self.stage / (import_library or library)).unlink() + with self.assertRaisesRegex(ValueError, "compiled Engine"): + package.check_contents(self.stage, metadata) + + def test_artifact_paths(self): + for name in ("../outside.a", "/outside.a", "C:/outside.lib", r"lib\engine.lib"): + with self.subTest(name=name), self.assertRaisesRegex(ValueError, "relative compiled Engine"): + package.check_contents(self.stage, {**self.metadata, "engine_library": name}) + + def make_link(self, path, target): + try: + path.symlink_to(target) + except (NotImplementedError, OSError) as error: + self.skipTest(f"Symbolic-link fixture unavailable: {error}") + + def test_shared_links(self): + library = self.stage / "lib/libcheryl-engine.so.4.0.0" + library.write_bytes(b"shared Engine") + soname = library.with_name("libcheryl-engine.so.4") + linker = library.with_name("libcheryl-engine.so") + self.make_link(soname, library.name) + self.make_link(linker, soname.name) + metadata = {**self.metadata, "linkage": "shared", "engine_library": "lib/" + library.name} + (self.stage / "SDK-MANIFEST.json").write_text(json.dumps(metadata), encoding="utf-8") + package.check_contents(self.stage, metadata) + destination = self.root / "shared.tar.gz" + package.archive_sdk(self.stage, destination) + with tarfile.open(destination) as archive: + member = archive.getmember("sdk/lib/" + linker.name) + self.assertTrue(member.issym()) + self.assertEqual(member.linkname, soname.name) + archive.extractall(self.root / "relocated", filter="data") + relocated = self.root / "relocated/sdk/lib" / linker.name + self.assertTrue(relocated.is_symlink()) + self.assertEqual(relocated.read_bytes(), b"shared Engine") + with self.assertRaisesRegex(ValueError, "supported by the archive"): + package.archive_sdk(self.stage, self.root / "shared.zip") + + def test_unsafe_links(self): + outside = self.root / "outside" + outside.write_bytes(b"outside SDK") + link = self.stage / "lib/unsafe" + for target, message in ((self.stage / self.metadata["engine_library"], "relative"), + (Path("../../outside"), "escapes"), (Path("missing"), "broken")): + with self.subTest(target=target): + self.make_link(link, target) + with self.assertRaisesRegex(ValueError, message): + package.check_contents(self.stage) + link.unlink() + def test_native_files(self): prefix = self.root / "native" kept = ("include/dependency.h", "lib/dependency.lib", "lib/cmake/dependency-release.cmake", @@ -167,7 +325,7 @@ def test_manifest(self): build.mkdir() identity = {"revision": "a" * 40, "submodules": {"extern/library": "b" * 40}} (source / "SOURCE-MANIFEST.json").write_text(json.dumps(identity), encoding="utf-8") - (build / "cheryl-sdk.json").write_text(json.dumps({"version": "4.0.0", "system": "Linux", + (build / "cheryl-sdk.json").write_text(json.dumps({**self.metadata, "architecture": "x86_64", "compiler": "GNU", "compiler_version": "14.2.0", "configuration": "Release"}), encoding="utf-8") @@ -180,9 +338,11 @@ def install(command, check): patch.object(package.platform, "freedesktop_os_release", return_value={"ID": "ubuntu", "VERSION_ID": "24.04"}), \ redirect_stdout(io.StringIO()): archive = package.bundle(source, build, output) - self.assertIn("4.0.0-sdk-linux-ubuntu24.04-x64-gnu14.2.0-release-aaaaaaaaaaaa", archive.name) + self.assertIn("4.0.0-sdk-linux-ubuntu24.04-x64-gnu14.2.0-release-static-aaaaaaaaaaaa", archive.name) manifest = json.loads((output / "SDK-MANIFEST.json").read_text(encoding="utf-8")) self.assertEqual(identity["submodules"], manifest["submodules"]) + self.assertEqual(manifest["component_versions"], {"engine": "4.0.0"}) + self.assertEqual(manifest["linkage"], "static") with tarfile.open(archive) as contents: names = contents.getnames() self.assertFalse(any(name.endswith((".cpp", ".c", ".exe")) for name in names)) diff --git a/.github/ci/test_compiler_cache.py b/.github/ci/test_compiler_cache.py index 42d23626..8d3f005a 100644 --- a/.github/ci/test_compiler_cache.py +++ b/.github/ci/test_compiler_cache.py @@ -1,8 +1,10 @@ -"""Check compiler cache layers using temporary files and mocked ccache commands.""" +"""Check compiler cache layers, worker artifacts and family publication.""" import importlib.util +import io import os from pathlib import Path, PurePosixPath, PureWindowsPath +import tarfile import tempfile import unittest from unittest.mock import patch @@ -14,8 +16,11 @@ compiler = importlib.util.module_from_spec(spec) spec.loader.exec_module(compiler) -PREFIX = "ccache-v2-ubuntu-24.04-X64-linux-gcc-hwloc-Release" +PREFIX = "ccache-v3-Linux-X64-linux-gcc14" ENTRY_KEY = "ab/" + "c" * 38 +OTHER_KEY = "ab/" + "d" * 38 +THIRD_KEY = "de/" + "a" * 38 +MANIFEST_KEY = "ef/" + "1" * 38 RESULT_HEADER = b"\xcc\xac\x01\x00" MANIFEST_HEADER = b"\xcc\xac\x01\x01" ENVIRONMENT = { @@ -35,10 +40,33 @@ def pull_event(base="develop", number=3): return {"number": number, "pull_request": {"number": number, "base": {"ref": base}}} -def write(directory, name, contents): +def write(directory, name, contents, *, mtime=None): path = directory / name path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(contents) + if mtime is not None: + os.utime(path, (mtime, mtime)) + return path + + +def snapshot(directory): + return {name: (path.read_bytes(), path.stat().st_mtime_ns) + for name, path in compiler._files(directory).items()} + + +def archive(path, entries): + """Write (name, bytes, mtime) entries or explicit TarInfo fixtures.""" + path.parent.mkdir(parents=True, exist_ok=True) + with tarfile.open(path, "w", format=tarfile.PAX_FORMAT) as output: + for entry in entries: + if isinstance(entry, tarfile.TarInfo): + output.addfile(entry) + continue + name, contents, mtime = entry + member = tarfile.TarInfo(name) + member.size = len(contents) + member.mtime = mtime + output.addfile(member, io.BytesIO(contents)) return path @@ -83,6 +111,8 @@ def test_paths(self): self.assertEqual(Path(main["current-path"]), self.root / "build/cache/compiler/main") self.assertEqual(Path(develop["current-path"]), self.root / "build/cache/compiler/develop") self.assertEqual(Path(pull["current-path"]), self.root / "build/cache/compiler/pr") + self.assertEqual(Path(pull["delta-path"]), self.root / "build/cache/compiler/delta") + self.assertEqual(Path(pull["delta-archive"]), self.root / "build/cache/compiler-artifacts/delta.tar") def test_keys(self): result = compiler.metadata(PREFIX, environment=pull_environment(), event=pull_event(), root=self.root) @@ -94,11 +124,18 @@ def test_keys(self): self.assertEqual(result["restore-develop"], "true") def test_prefix(self): - for prefix in ("ccache-v1-profile", "ccache-v2-../escape", "ccache-v2-profile|read-only", - "ccache-v2-profile with spaces"): + for prefix in ("ccache-v1-profile", "ccache-v2-ubuntu-24.04-X64-linux-gcc-hwloc-Release", + "ccache-v3-../escape", "ccache-v3-profile|read-only", "ccache-v3-profile with spaces", + "ccache-v3-Windows-X64-linux-gcc14", "ccache-v3-Linux-X64-windows-msvc"): with self.subTest(prefix=prefix), self.assertRaises(ValueError): compiler.metadata(prefix, environment=ENVIRONMENT, event={}, root=self.root) + def test_families(self): + for prefix in (PREFIX, "ccache-v3-Linux-X64-linux-clang18", "ccache-v3-Windows-X64-windows-msvc"): + with self.subTest(prefix=prefix): + result = compiler.metadata(prefix, environment=ENVIRONMENT, event={}, root=self.root) + self.assertEqual(result["current-key"], f"{prefix}-main-12345-2") + def test_run_identity(self): for name, value in (("GITHUB_RUN_ID", "0"), ("GITHUB_RUN_ID", "../escape"), ("GITHUB_RUN_ATTEMPT", "0"), ("GITHUB_RUN_ATTEMPT", "retry")): @@ -245,85 +282,335 @@ def setUp(self): self.event = pull_event() self.metadata = compiler.metadata(PREFIX, environment=self.environment, event=self.event, root=self.root) self.current = Path(self.metadata["current-path"]) + self.delta = Path(self.metadata["delta-path"]) + self.output = Path(self.metadata["delta-archive"]) + self.develop = Path(self.metadata["develop-path"]) + self.main = Path(self.metadata["main-path"]) + self.deltas = self.root / "artifacts" + + def configure(self): + return compiler.configure(environment=self.environment, event=self.event, root=self.root) + + def pack(self): + return compiler.pack(self.output, environment=self.environment, event=self.event, root=self.root) + + def publish(self): + return compiler.publish(self.deltas, environment=self.environment, event=self.event, root=self.root) + + def artifact(self, name, entries): + return archive(self.deltas / name / "delta.tar", entries) def test_configure(self): - with patch.object(compiler.subprocess, "run") as command: - result = compiler.configure(environment=self.environment, event=self.event, root=self.root) - command.assert_not_called() + write(self.current, ENTRY_KEY, RESULT_HEADER + b"previous PR result", mtime=10) + write(self.develop, OTHER_KEY, RESULT_HEADER + b"develop result", mtime=20) + write(self.main, THIRD_KEY, RESULT_HEADER + b"main result", mtime=30) + before = {path: snapshot(path) for path in (self.current, self.develop, self.main)} + result = self.configure() self.assertIn(result["CCACHE_REMOTE_ONLY"], ("1", "true")) self.assertNotEqual(Path(result["CCACHE_DIR"]), self.current) - storage = result["CCACHE_REMOTE_STORAGE"] - self.assertEqual(storage, compiler.storage_configuration( - self.current, [Path(self.metadata["develop-path"]), Path(self.metadata["main-path"])])) - self.assertTrue((self.root / "build/cache/compiler-state.json").is_file()) - - def test_cold_start(self): - with patch.object(compiler.subprocess, "run") as command: - compiler.configure(environment=self.environment, event=self.event, root=self.root) - write(self.current, "entries/result", b"new result") - result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) + self.assertEqual(result["CCACHE_REMOTE_STORAGE"], compiler.storage_configuration( + self.delta, [self.current, self.develop, self.main])) + self.assertEqual(compiler._entries(self.delta), {}) + for path, contents in before.items(): + self.assertEqual(snapshot(path), contents) + + def test_dirty_delta(self): + entry = write(self.delta, ENTRY_KEY, RESULT_HEADER + b"unfinished worker") + with self.assertRaises(ValueError): + self.configure() + self.assertEqual(entry.read_bytes(), RESULT_HEADER + b"unfinished worker") + + def test_pack(self): + self.configure() + write(self.current, ENTRY_KEY, RESULT_HEADER + b"restored result", mtime=10) + write(self.develop, OTHER_KEY, RESULT_HEADER + b"shared result", mtime=20) + write(self.main, THIRD_KEY, RESULT_HEADER + b"main result", mtime=30) + write(self.develop, MANIFEST_KEY, MANIFEST_HEADER + b"old refs", mtime=40) + before = {path: snapshot(path) for path in (self.current, self.develop, self.main)} + write(self.delta, ENTRY_KEY, RESULT_HEADER + b"other serialization") + write(self.delta, OTHER_KEY, RESULT_HEADER + b"shared serialization") + write(self.delta, THIRD_KEY, RESULT_HEADER + b"main serialization") + write(self.delta, MANIFEST_KEY, MANIFEST_HEADER + b"new refs", mtime=50) + result = self.pack() + self.assertEqual(result, {"has-content": "true", "removed": "3", "trimmed": "0"}) + with tarfile.open(self.output, "r:") as packed: + self.assertEqual(packed.getnames(), [MANIFEST_KEY]) + self.assertEqual(packed.extractfile(MANIFEST_KEY).read(), MANIFEST_HEADER + b"new refs") + for path, contents in before.items(): + self.assertEqual(snapshot(path), contents) + + def test_pack_mtime(self): + self.configure() + timestamp = 1_600_000_000.125 + original = write(self.delta, ENTRY_KEY, RESULT_HEADER + b"new result", mtime=timestamp) + original_mtime = original.stat().st_mtime_ns + self.assertEqual(self.pack()["has-content"], "true") + with tarfile.open(self.output, "r:") as packed: + self.assertEqual(packed.getmember(ENTRY_KEY).mtime, timestamp) + target = self.deltas / "worker" / "delta.tar" + target.parent.mkdir(parents=True) + self.output.rename(target) + self.assertEqual(self.publish()["changed"], "true") + self.assertEqual((self.current / ENTRY_KEY).stat().st_mtime_ns, original_mtime) + + def test_pack_shadow(self): + self.configure() + write(self.current, MANIFEST_KEY, MANIFEST_HEADER + b"current refs", mtime=10) + write(self.develop, MANIFEST_KEY, MANIFEST_HEADER + b"selected refs", mtime=10) + write(self.delta, MANIFEST_KEY, MANIFEST_HEADER + b"selected refs", mtime=20) + self.assertEqual(self.pack()["removed"], "0") + with tarfile.open(self.output, "r:") as packed: + self.assertEqual(packed.extractfile(MANIFEST_KEY).read(), MANIFEST_HEADER + b"selected refs") + + def test_pack_empty(self): + self.configure() + write(self.output.parent, self.output.name, b"stale archive") + result = self.pack() + self.assertEqual(result, {"has-content": "false", "removed": "0", "trimmed": "0"}) + self.assertFalse(self.output.exists()) + + def test_pack_path(self): + self.configure() + outside = self.root / "outside.tar" + with self.assertRaises(ValueError): + compiler.pack(outside, environment=self.environment, event=self.event, root=self.root) + self.assertFalse(outside.exists()) + + def test_pack_format(self): + self.configure() + write(self.delta, "unexpected/file", b"not a native cache entry") + with self.assertRaises(ValueError): + self.pack() + self.assertFalse(self.output.exists()) + + def test_publish_union(self): + original = write(self.current, ENTRY_KEY, RESULT_HEADER + b"original result", mtime=10) + write(self.current, MANIFEST_KEY, MANIFEST_HEADER + b"old refs", mtime=10) + self.artifact("a-worker", [ + (ENTRY_KEY, RESULT_HEADER + b"other serialization", 30), + (OTHER_KEY, RESULT_HEADER + b"first job", 20), + (MANIFEST_KEY, MANIFEST_HEADER + b"newest complete refs", 40), + ]) + self.artifact("z-worker", [ + (THIRD_KEY, RESULT_HEADER + b"second job", 25), + (MANIFEST_KEY, MANIFEST_HEADER + b"older complete refs", 35), + ]) + result = self.publish() self.assertEqual(result["changed"], "true") - self.assertEqual(result["removed"], "0") - self.assertEqual(command.call_count, 1) - self.assertEqual(command.call_args.args[0], ["ccache", "--trim-dir", str(self.current), - "--trim-max-size", "1GiB", "--trim-method", "mtime"]) - self.assertTrue(command.call_args.kwargs["check"]) + self.assertEqual(result["archives"], "2") + self.assertEqual(set(compiler._entries(self.current)), {ENTRY_KEY, OTHER_KEY, THIRD_KEY, MANIFEST_KEY}) + self.assertEqual(original.read_bytes(), RESULT_HEADER + b"original result") + self.assertEqual(original.stat().st_mtime_ns, 30_000_000_000) + self.assertEqual((self.current / OTHER_KEY).read_bytes(), RESULT_HEADER + b"first job") + self.assertEqual((self.current / THIRD_KEY).read_bytes(), RESULT_HEADER + b"second job") + self.assertEqual((self.current / MANIFEST_KEY).read_bytes(), MANIFEST_HEADER + b"newest complete refs") + + def test_manifest_tie(self): + self.artifact("z-worker", [(MANIFEST_KEY, MANIFEST_HEADER + b"z refs", 20)]) + self.artifact("a-worker", [(MANIFEST_KEY, MANIFEST_HEADER + b"a refs", 20)]) + self.publish() + self.assertEqual((self.current / MANIFEST_KEY).read_bytes(), MANIFEST_HEADER + b"z refs") + + def test_publish_parents(self): + write(self.develop, ENTRY_KEY, RESULT_HEADER + b"parent result", mtime=10) + write(self.main, THIRD_KEY, RESULT_HEADER + b"main result", mtime=20) + before = {path: snapshot(path) for path in (self.develop, self.main)} + self.artifact("worker", [ + (ENTRY_KEY, RESULT_HEADER + b"other serialization", 30), + (THIRD_KEY, RESULT_HEADER + b"main serialization", 30), + (OTHER_KEY, RESULT_HEADER + b"own result", 30), + ]) + result = self.publish() + self.assertEqual(result["changed"], "true") + self.assertEqual(result["removed"], "2") + self.assertEqual(set(compiler._entries(self.current)), {OTHER_KEY}) + for path, contents in before.items(): + self.assertEqual(snapshot(path), contents) + + def test_publish_trim(self): + write(self.current, ENTRY_KEY, RESULT_HEADER + b"old", mtime=10) + newest = RESULT_HEADER + b"newest" + self.artifact("worker", [(OTHER_KEY, newest, 20)]) + trim = compiler.trim + with patch.object(compiler, "trim", side_effect=lambda directory: trim(directory, limit=len(newest))): + result = self.publish() + self.assertEqual(result["trimmed"], "1") + self.assertEqual(result["changed"], "true") + self.assertEqual(set(compiler._entries(self.current)), {OTHER_KEY}) + self.assertEqual((self.current / OTHER_KEY).read_bytes(), newest) - def test_hit_only(self): - path = write(self.current, "entries/result", b"own result") - with patch.object(compiler.subprocess, "run"): - compiler.configure(environment=self.environment, event=self.event, root=self.root) - os.utime(path, (1_600_000_000, 1_600_000_000)) - result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) + def test_unchanged(self): + write(self.current, ENTRY_KEY, RESULT_HEADER + b"own result", mtime=10) + before = snapshot(self.current) + result = self.publish() self.assertEqual(result["changed"], "false") - - def test_parent_subtraction(self): - parent = Path(self.metadata["develop-path"]) - parent_file = write(parent, "entries/result", b"shared result") - with patch.object(compiler.subprocess, "run"): - compiler.configure(environment=self.environment, event=self.event, root=self.root) - duplicate = write(self.current, "entries/result", b"shared result") - write(self.current, "entries/new", b"own result") - result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) - self.assertEqual(result, {"changed": "true", "removed": "1"}) - self.assertFalse(duplicate.exists()) - self.assertEqual(parent_file.read_bytes(), b"shared result") + self.assertEqual(result["has-content"], "true") + self.assertEqual(result["archives"], "0") + self.assertEqual(snapshot(self.current), before) def test_empty_layer(self): - with patch.object(compiler.subprocess, "run"): - compiler.configure(environment=self.environment, event=self.event, root=self.root) - result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) - self.assertEqual(result["changed"], "false") + result = self.publish() + self.assertEqual(result["changed"], "true") + self.assertEqual(result["has-content"], "true") + self.assertTrue((self.current / compiler.EMPTY_MARKER).exists()) + again = self.publish() + self.assertEqual(again["changed"], "false") + self.assertEqual(again["has-content"], "true") def test_empty_replacement(self): - write(Path(self.metadata["develop-path"]), "entries/result", b"shared result") - write(self.current, "entries/result", b"shared result") - empty = self.root / "empty" - empty.mkdir() - with patch.object(compiler.subprocess, "run"): - compiler.configure(environment=self.environment, event=self.event, root=self.root) - result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) - self.assertEqual(result, {"changed": "true", "removed": "1"}) - self.assertTrue((self.current / ".cheryl-empty-layer").is_file()) - self.assertEqual(compiler.fingerprint(self.current), compiler.fingerprint(empty)) + write(self.develop, ENTRY_KEY, RESULT_HEADER + b"parent result", mtime=10) + write(self.current, ENTRY_KEY, RESULT_HEADER + b"own serialization", mtime=20) + before = snapshot(self.develop) + result = self.publish() + self.assertEqual(result["changed"], "true") + self.assertEqual(result["has-content"], "true") + self.assertEqual(result["removed"], "1") + self.assertTrue((self.current / compiler.EMPTY_MARKER).is_file()) + self.assertEqual(compiler._entries(self.current), {}) + self.assertEqual(snapshot(self.develop), before) def test_restored_empty(self): - write(self.current, ".cheryl-empty-layer", b"") - with patch.object(compiler.subprocess, "run"): - compiler.configure(environment=self.environment, event=self.event, root=self.root) - result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) + marker = write(self.current, compiler.EMPTY_MARKER, b"restored tombstone", mtime=10) + result = self.publish() self.assertEqual(result["changed"], "false") - self.assertEqual(result["removed"], "0") + self.assertEqual(result["has-content"], "true") + self.assertTrue(marker.is_file()) def test_new_entries(self): - marker = write(self.current, ".cheryl-empty-layer", b"") - with patch.object(compiler.subprocess, "run"): - compiler.configure(environment=self.environment, event=self.event, root=self.root) - entry = write(self.current, "entries/result", b"own result") - result = compiler.prepare_save(environment=self.environment, event=self.event, root=self.root) + marker = write(self.current, compiler.EMPTY_MARKER, b"") + self.artifact("worker", [(ENTRY_KEY, RESULT_HEADER + b"new result", 20)]) + result = self.publish() self.assertEqual(result["changed"], "true") self.assertFalse(marker.exists()) - self.assertEqual(entry.read_bytes(), b"own result") + self.assertEqual((self.current / ENTRY_KEY).read_bytes(), RESULT_HEADER + b"new result") + + def test_trim(self): + write(self.current, ENTRY_KEY, b"12345", mtime=10) + write(self.current, OTHER_KEY, b"123456", mtime=20) + write(self.current, THIRD_KEY, b"1234567", mtime=30) + self.assertEqual(compiler.trim(self.current, limit=13), 1) + self.assertEqual(set(compiler._entries(self.current)), {OTHER_KEY, THIRD_KEY}) + + def test_legacy_plan(self): + prefix = "ccache-v2-ubuntu-24.04-X64-linux-gcc-hwloc-Release" + result = compiler.migration_plan(prefix, environment=self.environment, event=self.event, root=self.root) + self.assertEqual(result, {"path": str(self.current), "prefix": prefix + "-pr-", + "key": prefix + "-pr-12345-2"}) + self.assertEqual(self.environment["CHERYL_CCACHE_PREFIX"], PREFIX) + + def test_legacy_family(self): + for prefix in ("ccache-v2-ubuntu-24.04-X64-linux-clang-debug-Debug", + "ccache-v2-windows-2025-X64-windows-msvc-hwloc-RelWithDebInfo", + "ccache-v2-ubuntu-24.04-ARM64-linux-gcc-hwloc-Release", + "ccache-v2-ubuntu-24.04-X64-unknown-Release"): + with self.subTest(prefix=prefix), self.assertRaises(ValueError): + compiler.migration_plan(prefix, environment=self.environment, event=self.event, root=self.root) + + def test_legacy_profiles(self): + profiles = ( + (PREFIX, "ubuntu-24.04", "linux-gcc-hwloc-Release"), + (PREFIX, "ubuntu-24.04", "linux-gcc-neutral-Release"), + (PREFIX, "ubuntu-24.04", "linux-modules-Release"), + (PREFIX, "ubuntu-24.04", "linux-modules-shared-Release"), + ("ccache-v3-Linux-X64-linux-clang18", "ubuntu-24.04", "linux-clang-debug-Debug"), + ("ccache-v3-Windows-X64-windows-msvc", "windows-2025", "windows-msvc-hwloc-RelWithDebInfo"), + ("ccache-v3-Windows-X64-windows-msvc", "windows-2025", "windows-modules-RelWithDebInfo"), + ("ccache-v3-Windows-X64-windows-msvc", "windows-2025", "windows-modules-shared-RelWithDebInfo"), + ) + for family, runner, profile in profiles: + with self.subTest(family=family, profile=profile): + prefix = f"ccache-v2-{runner}-X64-{profile}" + environment = {**self.environment, "CHERYL_CCACHE_PREFIX": family} + result = compiler.migration_plan(prefix, environment=environment, event=self.event, root=self.root) + self.assertEqual(result["path"], str(self.current)) + self.assertEqual(result["prefix"], prefix + "-pr-") + + def test_legacy_union(self): + write(self.develop, THIRD_KEY, RESULT_HEADER + b"develop result", mtime=5) + parent = snapshot(self.develop) + write(self.current, ENTRY_KEY, RESULT_HEADER + b"first profile", mtime=10) + write(self.current, MANIFEST_KEY, MANIFEST_HEADER + b"first refs", mtime=10) + first = compiler.import_legacy(environment=self.environment, event=self.event, root=self.root) + self.assertEqual(first["imported"], "2") + self.assertEqual(compiler._entries(self.current), {}) + write(self.current, OTHER_KEY, RESULT_HEADER + b"second profile", mtime=20) + write(self.current, MANIFEST_KEY, MANIFEST_HEADER + b"second refs", mtime=20) + second = compiler.import_legacy(environment=self.environment, event=self.event, root=self.root) + self.assertEqual(second["imported"], "2") + self.assertEqual(compiler._entries(self.current), {}) + result = self.publish() + self.assertEqual(result["changed"], "true") + self.assertEqual(result["archives"], "0") + self.assertEqual(set(compiler._entries(self.current)), {ENTRY_KEY, OTHER_KEY, MANIFEST_KEY}) + self.assertEqual((self.current / MANIFEST_KEY).read_bytes(), MANIFEST_HEADER + b"second refs") + self.assertEqual(snapshot(self.develop), parent) + + def test_legacy_empty(self): + marker = write(self.current, compiler.EMPTY_MARKER, b"old empty snapshot") + result = compiler.import_legacy(environment=self.environment, event=self.event, root=self.root) + self.assertEqual(result["imported"], "0") + self.assertFalse(marker.exists()) + published = self.publish() + self.assertEqual(published["changed"], "true") + self.assertEqual(published["has-content"], "true") + + def test_archive_links(self): + write(self.develop, OTHER_KEY, RESULT_HEADER + b"parent result", mtime=10) + protected = snapshot(self.develop) + outside = write(self.root, "outside", b"protected outside file") + for kind in (tarfile.SYMTYPE, tarfile.LNKTYPE): + with self.subTest(kind=kind): + member = tarfile.TarInfo(ENTRY_KEY) + member.type = kind + member.linkname = str(outside) + self.artifact("unsafe", [member]) + with self.assertRaises(ValueError): + self.publish() + self.assertEqual(outside.read_bytes(), b"protected outside file") + self.assertEqual(snapshot(self.develop), protected) + self.assertEqual(compiler._entries(self.current), {}) + + def test_archive_paths(self): + outside = write(self.root, "outside", b"protected outside file") + for name in ("../outside", "/outside", r"C:\outside", ENTRY_KEY + "/../outside"): + with self.subTest(name=name): + self.artifact("unsafe", [(OTHER_KEY, RESULT_HEADER + b"valid", 10), (name, b"bad", 10)]) + with self.assertRaises(ValueError): + self.publish() + self.assertEqual(outside.read_bytes(), b"protected outside file") + self.assertEqual(compiler._entries(self.current), {}) + + def test_archive_duplicate(self): + self.artifact("unsafe", [(ENTRY_KEY, b"first", 10), (ENTRY_KEY, b"second", 20)]) + with self.assertRaises(ValueError): + self.publish() + self.assertEqual(compiler._entries(self.current), {}) + + def test_archive_size(self): + self.artifact("unsafe", [(ENTRY_KEY, b"12345", 10), (OTHER_KEY, b"67890", 20)]) + with patch.object(compiler, "LAYER_LIMIT", 9), self.assertRaises(ValueError): + self.publish() + self.assertEqual(compiler._entries(self.current), {}) + + def test_archive_count(self): + self.artifact("unsafe", [(ENTRY_KEY, b"first", 10), (OTHER_KEY, b"second", 20)]) + with patch.object(compiler, "ARCHIVE_ENTRY_LIMIT", 1), self.assertRaises(ValueError): + self.publish() + self.assertEqual(compiler._entries(self.current), {}) + + def test_validate_archives(self): + write(self.current, ENTRY_KEY, RESULT_HEADER + b"restored result", mtime=5) + before = snapshot(self.current) + self.artifact("a-valid", [(OTHER_KEY, RESULT_HEADER + b"new result", 10)]) + self.artifact("z-unsafe", [("../outside", b"bad", 10)]) + with self.assertRaises(ValueError): + self.publish() + self.assertEqual(snapshot(self.current), before) + + def test_artifact_name(self): + write(self.deltas, "unexpected.txt", b"not a worker archive") + with self.assertRaises(ValueError): + self.publish() if __name__ == "__main__": diff --git a/.github/ci/verify_sdk.py b/.github/ci/verify_sdk.py index 5edc02ea..7d593f46 100644 --- a/.github/ci/verify_sdk.py +++ b/.github/ci/verify_sdk.py @@ -19,52 +19,57 @@ def main(): parser.add_argument("--build-dir", type=Path, required=True) parser.add_argument("--producer-build", type=Path, required=True) parser.add_argument("--jobs", type=int, default=os.cpu_count() or 1) + parser.add_argument("--phase", choices=("all", "build", "test"), default="all") args = parser.parse_args() source = args.source.resolve() packages = args.packages.resolve() build = args.build_dir.resolve() - digest, name = (packages / "SHA256SUMS").read_text(encoding="utf-8").strip().split(" ") - archive_path = packages / name - if archive_path.parent != packages: - raise ValueError("Invalid archive filename") - with archive_path.open("rb") as contents: - if hashlib.file_digest(contents, "sha256").hexdigest() != digest: - raise ValueError("SDK checksum mismatch") extracted = build.parent / (build.name + "-relocated-sdk") - extracted.mkdir(parents=True) - if archive_path.suffix == ".zip": - with zipfile.ZipFile(archive_path) as archive: - for name in archive.namelist(): - if not (extracted / name).resolve().is_relative_to(extracted.resolve()): - raise ValueError("Invalid SDK archive path") - archive.extractall(extracted) - else: - with tarfile.open(archive_path) as archive: - archive.extractall(extracted, filter="data") + if args.phase != "test": + digest, name = (packages / "SHA256SUMS").read_text(encoding="utf-8").strip().split(" ") + archive_path = packages / name + if archive_path.parent != packages: + raise ValueError("Invalid archive filename") + with archive_path.open("rb") as contents: + if hashlib.file_digest(contents, "sha256").hexdigest() != digest: + raise ValueError("SDK checksum mismatch") + extracted.mkdir(parents=True) + if archive_path.suffix == ".zip": + with zipfile.ZipFile(archive_path) as archive: + for name in archive.namelist(): + if not (extracted / name).resolve().is_relative_to(extracted.resolve()): + raise ValueError("Invalid SDK archive path") + archive.extractall(extracted) + else: + with tarfile.open(archive_path) as archive: + archive.extractall(extracted, filter="data") roots = list(extracted.iterdir()) if len(roots) != 1: raise ValueError("Expected one SDK root") sdk = roots[0] - check_contents(sdk) metadata = json.loads((sdk / "SDK-MANIFEST.json").read_text(encoding="utf-8")) - # Source and producer build paths must never be required by the import files. - for config in sdk.rglob("*.cmake"): - contents = config.read_text(encoding="utf-8") - if any(path.resolve().as_posix() in contents for path in (source, args.producer_build)): - raise ValueError(f"SDK leaks a producer path: {config}") + if args.phase != "test": + check_contents(sdk, metadata) + # Source and producer build paths must never be required by the import files. + for config in sdk.rglob("*.cmake"): + contents = config.read_text(encoding="utf-8") + if any(path.resolve().as_posix() in contents for path in (source, args.producer_build)): + raise ValueError(f"SDK leaks a producer path: {config}") environment = os.environ.copy() if metadata["system"] == "Windows": - environment["PATH"] = str(sdk / "dependencies/bin") + os.pathsep + environment["PATH"] - commands = [ - ["cmake", "-S", source / ".github/ci/sdk-consumer", "-B", build, "-G", "Ninja", - f"-DCMAKE_PREFIX_PATH={sdk}", f"-DCHERYL_REPOSITORY_ROOT={source.as_posix()}", - f"-DCMAKE_BUILD_TYPE={metadata['configuration']}"], - ["cmake", "--build", build, "--parallel", str(args.jobs)], - ["ctest", "--test-dir", build, "--parallel", str(args.jobs), "--no-tests=error", "--timeout", "120", - "--output-on-failure", "--output-junit", build / "junit.xml"]] - if metadata["system"] == "Windows" and environment.get("CMAKE_CXX_COMPILER_LAUNCHER") == "ccache": - commands[0] += ["-DCMAKE_POLICY_DEFAULT_CMP0141=NEW", - "-DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=$<$:Embedded>"] + environment["PATH"] = os.pathsep.join((str(sdk / "bin"), str(sdk / "dependencies/bin"), environment.get("PATH", ""))) + commands = [] + if args.phase != "test": + configure = ["cmake", "-S", source / ".github/ci/sdk-consumer", "-B", build, "-G", "Ninja", + f"-DCMAKE_PREFIX_PATH={sdk}", f"-DCHERYL_REPOSITORY_ROOT={source.as_posix()}", + f"-DCMAKE_BUILD_TYPE={metadata['configuration']}"] + if metadata["system"] == "Windows" and environment.get("CMAKE_CXX_COMPILER_LAUNCHER") == "ccache": + configure += ["-DCMAKE_POLICY_DEFAULT_CMP0141=NEW", + "-DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=$<$:Embedded>"] + commands.extend([configure, ["cmake", "--build", build, "--parallel", str(args.jobs)]]) + if args.phase != "build": + commands.append(["ctest", "--test-dir", build, "--parallel", str(args.jobs), "--no-tests=error", "--timeout", "120", + "--output-on-failure", "--output-junit", build / "junit.xml"]) for command in commands: print("+ " + subprocess.list2cmdline(list(map(str, command))), flush=True) subprocess.run(command, env=environment, check=True) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a73bb85..a7efaa43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,10 +28,10 @@ jobs: fail-fast: false matrix: include: - - {name: linux-gcc-hwloc, os: ubuntu-24.04, type: Release, hwloc: 'ON', cc: gcc-14, cxx: g++-14} - - {name: linux-gcc-neutral, os: ubuntu-24.04, type: Release, hwloc: 'OFF', cc: gcc-14, cxx: g++-14} - - {name: linux-clang-debug, os: ubuntu-24.04, type: Debug, hwloc: 'ON', cc: clang-18, cxx: clang++-18} - - {name: windows-msvc-hwloc, os: windows-2025, type: RelWithDebInfo, hwloc: 'ON', cc: cl, cxx: cl} + - {name: linux-gcc-hwloc, os: ubuntu-24.04, cache_family: linux-gcc14, type: Release, hwloc: 'ON', cc: gcc-14, cxx: g++-14} + - {name: linux-gcc-neutral, os: ubuntu-24.04, cache_family: linux-gcc14, type: Release, hwloc: 'OFF', cc: gcc-14, cxx: g++-14} + - {name: linux-clang-debug, os: ubuntu-24.04, cache_family: linux-clang18, type: Debug, hwloc: 'ON', cc: clang-18, cxx: clang++-18} + - {name: windows-msvc-hwloc, os: windows-2025, cache_family: windows-msvc, type: RelWithDebInfo, hwloc: 'ON', cc: cl, cxx: cl} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -45,7 +45,7 @@ jobs: id: ccache uses: ./.github/actions/restore-compiler-cache with: - prefix: ccache-v2-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.name }}-${{ matrix.type }} + prefix: ccache-v3-${{ runner.os }}-${{ runner.arch }}-${{ matrix.cache_family }} - name: Reset compiler cache statistics id: ccache-ready run: ccache --zero-stats @@ -56,7 +56,7 @@ jobs: python .github/ci/test_cache_cleanup.py python .github/ci/test_compiler_cache.py python .github/ci/test_dependencies.py - - name: Build and test Linux + - name: Build Linux if: runner.os == 'Linux' env: CC: ${{ matrix.cc }} @@ -64,8 +64,8 @@ jobs: BUILD_TYPE: ${{ matrix.type }} HWLOC: ${{ matrix.hwloc }} CHERYL_STB_ALLOCATION_CFF: /usr/share/fonts/opentype/urw-base35/NimbusSans-Regular.otf - run: python .github/ci/run.py --build-dir build/ci --profile headless --build-type "$BUILD_TYPE" --hwloc "$HWLOC" - - name: Build and test Windows + run: python .github/ci/run.py --phase build --build-dir build/ci --profile headless --build-type "$BUILD_TYPE" --hwloc "$HWLOC" + - name: Build Windows if: runner.os == 'Windows' shell: pwsh env: @@ -76,7 +76,23 @@ jobs: $vs = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath if (!$vs) { throw 'MSVC x64 tools are unavailable' } & "$vs/Common7/Tools/Launch-VsDevShell.ps1" -Arch amd64 -HostArch amd64 -SkipAutomaticLocation - python .github/ci/run.py --build-dir build/ci --profile headless --build-type $env:BUILD_TYPE --hwloc $env:HWLOC --vcpkg-root $env:CHERYL_CI_VCPKG_ROOT + python .github/ci/run.py --phase build --build-dir build/ci --profile headless --build-type $env:BUILD_TYPE --hwloc $env:HWLOC --vcpkg-root $env:CHERYL_CI_VCPKG_ROOT + if ($LASTEXITCODE) { exit $LASTEXITCODE } + - name: Test Linux + if: runner.os == 'Linux' + env: + BUILD_TYPE: ${{ matrix.type }} + HWLOC: ${{ matrix.hwloc }} + CHERYL_STB_ALLOCATION_CFF: /usr/share/fonts/opentype/urw-base35/NimbusSans-Regular.otf + run: python .github/ci/run.py --phase test --build-dir build/ci --profile headless --build-type "$BUILD_TYPE" --hwloc "$HWLOC" + - name: Test Windows + if: runner.os == 'Windows' + shell: pwsh + env: + BUILD_TYPE: ${{ matrix.type }} + HWLOC: ${{ matrix.hwloc }} + run: | + python .github/ci/run.py --phase test --build-dir build/ci --profile headless --build-type $env:BUILD_TYPE --hwloc $env:HWLOC if ($LASTEXITCODE) { exit $LASTEXITCODE } - name: Check dependency cache updates if: ${{ !cancelled() && runner.os == 'Windows' && steps.dependencies.outcome == 'success' && steps.dependencies.outputs.dependency-cache-writer == 'true' }} @@ -91,17 +107,23 @@ jobs: - name: Report compiler cache statistics if: ${{ !cancelled() && steps.ccache-ready.outcome == 'success' }} run: ccache --show-stats --verbose - - name: Prepare compiler cache additions + - name: Pack compiler cache additions if: ${{ !cancelled() && steps.ccache-ready.outcome == 'success' }} id: compiler-cache-state shell: bash - run: python .github/ci/compiler_cache.py prepare-save - - name: Save compiler cache - if: ${{ !cancelled() && steps.compiler-cache-state.outputs.changed == 'true' }} - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + env: + CCACHE_DELTA_ARCHIVE: ${{ steps.ccache.outputs.delta-archive }} + run: python .github/ci/compiler_cache.py pack --output "$CCACHE_DELTA_ARCHIVE" + - name: Retain compiler cache additions + if: ${{ !cancelled() && steps.compiler-cache-state.outputs.has-content == 'true' }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - path: ${{ steps.ccache.outputs.current-path }} - key: ${{ steps.ccache.outputs.current-key }} + name: compiler-cache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.cache_family }}-${{ matrix.name }} + path: ${{ steps.ccache.outputs.delta-archive }} + if-no-files-found: error + compression-level: 0 + overwrite: true + retention-days: 1 - name: Retain results if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 @@ -122,8 +144,10 @@ jobs: fail-fast: false matrix: include: - - {name: linux-modules, os: ubuntu-24.04, type: Release} - - {name: windows-modules, os: windows-2025, type: RelWithDebInfo} + - {name: linux-modules, os: ubuntu-24.04, cache_family: linux-gcc14, type: Release, linkage: static} + - {name: windows-modules, os: windows-2025, cache_family: windows-msvc, type: RelWithDebInfo, linkage: static} + - {name: linux-modules-shared, os: ubuntu-24.04, cache_family: linux-gcc14, type: Release, linkage: shared} + - {name: windows-modules-shared, os: windows-2025, cache_family: windows-msvc, type: RelWithDebInfo, linkage: shared} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -137,33 +161,58 @@ jobs: id: ccache uses: ./.github/actions/restore-compiler-cache with: - prefix: ccache-v2-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.name }}-${{ matrix.type }} + prefix: ccache-v3-${{ runner.os }}-${{ runner.arch }}-${{ matrix.cache_family }} - name: Reset compiler cache statistics id: ccache-ready run: ccache --zero-stats - - name: Build and test Linux modules + - name: Build Linux modules and SDK consumers if: runner.os == 'Linux' env: CC: gcc-14 CXX: g++-14 + BUILD_TYPE: ${{ matrix.type }} + LINKAGE: ${{ matrix.linkage }} CHERYL_STB_ALLOCATION_CFF: /usr/share/fonts/opentype/urw-base35/NimbusSans-Regular.otf run: | - python .github/ci/run.py --build-dir build/ci --profile modules --hwloc ON --sdk + python .github/ci/run.py --phase build --build-dir build/ci --profile modules --build-type "$BUILD_TYPE" --linkage "$LINKAGE" --hwloc ON --sdk python .github/ci/package.py --build-dir build/ci --output build/packages - python .github/ci/verify_sdk.py --packages build/packages --build-dir build/sdk-consumer --producer-build build/ci - - name: Build and test Windows modules + python .github/ci/verify_sdk.py --phase build --packages build/packages --build-dir build/sdk-consumer --producer-build build/ci + - name: Build Windows modules and SDK consumers if: runner.os == 'Windows' shell: pwsh + env: + BUILD_TYPE: ${{ matrix.type }} + LINKAGE: ${{ matrix.linkage }} run: | $vswhere = "${env:ProgramFiles(x86)}/Microsoft Visual Studio/Installer/vswhere.exe" $vs = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath if (!$vs) { throw 'MSVC x64 tools are unavailable' } & "$vs/Common7/Tools/Launch-VsDevShell.ps1" -Arch amd64 -HostArch amd64 -SkipAutomaticLocation - python .github/ci/run.py --build-dir build/ci --profile modules --build-type RelWithDebInfo --hwloc ON --sdk --vcpkg-root $env:CHERYL_CI_VCPKG_ROOT + python .github/ci/run.py --phase build --build-dir build/ci --profile modules --build-type $env:BUILD_TYPE --linkage $env:LINKAGE --hwloc ON --sdk --vcpkg-root $env:CHERYL_CI_VCPKG_ROOT if ($LASTEXITCODE) { exit $LASTEXITCODE } python .github/ci/package.py --build-dir build/ci --output build/packages if ($LASTEXITCODE) { exit $LASTEXITCODE } - python .github/ci/verify_sdk.py --packages build/packages --build-dir build/sdk-consumer --producer-build build/ci + python .github/ci/verify_sdk.py --phase build --packages build/packages --build-dir build/sdk-consumer --producer-build build/ci + if ($LASTEXITCODE) { exit $LASTEXITCODE } + - name: Test Linux modules and SDK consumers + if: runner.os == 'Linux' + env: + BUILD_TYPE: ${{ matrix.type }} + LINKAGE: ${{ matrix.linkage }} + CHERYL_STB_ALLOCATION_CFF: /usr/share/fonts/opentype/urw-base35/NimbusSans-Regular.otf + run: | + python .github/ci/run.py --phase test --build-dir build/ci --profile modules --build-type "$BUILD_TYPE" --linkage "$LINKAGE" --hwloc ON --sdk + python .github/ci/verify_sdk.py --phase test --packages build/packages --build-dir build/sdk-consumer --producer-build build/ci + - name: Test Windows modules and SDK consumers + if: runner.os == 'Windows' + shell: pwsh + env: + BUILD_TYPE: ${{ matrix.type }} + LINKAGE: ${{ matrix.linkage }} + run: | + python .github/ci/run.py --phase test --build-dir build/ci --profile modules --build-type $env:BUILD_TYPE --linkage $env:LINKAGE --hwloc ON --sdk + if ($LASTEXITCODE) { exit $LASTEXITCODE } + python .github/ci/verify_sdk.py --phase test --packages build/packages --build-dir build/sdk-consumer --producer-build build/ci if ($LASTEXITCODE) { exit $LASTEXITCODE } - name: Check dependency cache updates if: ${{ !cancelled() && runner.os == 'Windows' && steps.dependencies.outcome == 'success' && steps.dependencies.outputs.dependency-cache-writer == 'true' }} @@ -178,17 +227,23 @@ jobs: - name: Report compiler cache statistics if: ${{ !cancelled() && steps.ccache-ready.outcome == 'success' }} run: ccache --show-stats --verbose - - name: Prepare compiler cache additions + - name: Pack compiler cache additions if: ${{ !cancelled() && steps.ccache-ready.outcome == 'success' }} id: compiler-cache-state shell: bash - run: python .github/ci/compiler_cache.py prepare-save - - name: Save compiler cache - if: ${{ !cancelled() && steps.compiler-cache-state.outputs.changed == 'true' }} - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + env: + CCACHE_DELTA_ARCHIVE: ${{ steps.ccache.outputs.delta-archive }} + run: python .github/ci/compiler_cache.py pack --output "$CCACHE_DELTA_ARCHIVE" + - name: Retain compiler cache additions + if: ${{ !cancelled() && steps.compiler-cache-state.outputs.has-content == 'true' }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - path: ${{ steps.ccache.outputs.current-path }} - key: ${{ steps.ccache.outputs.current-key }} + name: compiler-cache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.cache_family }}-${{ matrix.name }} + path: ${{ steps.ccache.outputs.delta-archive }} + if-no-files-found: error + compression-level: 0 + overwrite: true + retention-days: 1 - name: Retain checked SDK candidate if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 @@ -212,6 +267,80 @@ jobs: if-no-files-found: warn retention-days: 14 + compiler-cache: + name: Publish compiler cache (${{ matrix.cache_family }}) + if: ${{ !cancelled() }} + needs: [headless, modules] + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + cache_family: linux-gcc14 + legacy_1: linux-gcc-hwloc-Release + legacy_2: linux-gcc-neutral-Release + legacy_3: linux-modules-Release + legacy_4: linux-modules-shared-Release + - os: ubuntu-24.04 + cache_family: linux-clang18 + legacy_1: linux-clang-debug-Debug + legacy_2: '' + legacy_3: '' + legacy_4: '' + - os: windows-2025 + cache_family: windows-msvc + legacy_1: windows-msvc-hwloc-RelWithDebInfo + legacy_2: windows-modules-RelWithDebInfo + legacy_3: windows-modules-shared-RelWithDebInfo + legacy_4: '' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Restore compiler cache layers + id: ccache + uses: ./.github/actions/restore-compiler-cache + with: + prefix: ccache-v3-${{ runner.os }}-${{ runner.arch }}-${{ matrix.cache_family }} + attach: 'false' + - name: Import previous compiler cache (1) + if: steps.ccache.outputs.current-hit != 'true' && matrix.legacy_1 != '' + uses: ./.github/actions/migrate-compiler-cache + with: + legacy-prefix: ccache-v2-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.legacy_1 }} + - name: Import previous compiler cache (2) + if: steps.ccache.outputs.current-hit != 'true' && matrix.legacy_2 != '' + uses: ./.github/actions/migrate-compiler-cache + with: + legacy-prefix: ccache-v2-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.legacy_2 }} + - name: Import previous compiler cache (3) + if: steps.ccache.outputs.current-hit != 'true' && matrix.legacy_3 != '' + uses: ./.github/actions/migrate-compiler-cache + with: + legacy-prefix: ccache-v2-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.legacy_3 }} + - name: Import previous compiler cache (4) + if: steps.ccache.outputs.current-hit != 'true' && matrix.legacy_4 != '' + uses: ./.github/actions/migrate-compiler-cache + with: + legacy-prefix: ccache-v2-${{ matrix.os }}-${{ runner.arch }}-${{ matrix.legacy_4 }} + - name: Collect compiler cache additions + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: compiler-cache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.cache_family }}-* + path: build/cache/compiler-deltas + - name: Merge compiler cache additions + id: compiler-cache-state + shell: bash + run: python .github/ci/compiler_cache.py publish --deltas build/cache/compiler-deltas + - name: Save compiler cache family + if: steps.compiler-cache-state.outputs.changed == 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.ccache.outputs.current-path }} + key: ${{ steps.ccache.outputs.current-key }} + sdk: name: Collect accepted development SDKs if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/CMakeLists.txt b/CMakeLists.txt index b0f7a613..516bfff1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ endif() # Cheryl build metadata: options, target identities, artifact names, and linkage declarations. include(cmake/CherylOptions.cmake) cheryl_configure_root_options() +include(cmake/CherylLibraries.cmake) include(cmake/CherylTargets.cmake) include(cmake/CherylOutputs.cmake) include(cmake/CherylLinkage.cmake) diff --git a/README.md b/README.md index 4d45f4c7..475612de 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ application. [Read the documentation](https://cheryl-engine.readthedocs.io/) for the API reference, setup guides and first-game walkthrough. -The default assembly includes Native GLFW, OpenGL, TGUI and the windowed demo. -RmlUi and miniaudio are independently selectable and default to `OFF`. -Linux GNU/Clang also selects an optional output-terminal module; its implementation -is linked automatically only in actual Debug configurations. +The default assembly includes Native GLFW, OpenGL, TGUI, RmlUi and miniaudio, +plus hwloc and JSON worker support. The demo and tests are opt-in. +Single-configuration Linux GNU/Clang Debug builds also select the optional +output-terminal module by default. Other configurations default it to `OFF`. ## Contents @@ -27,7 +27,10 @@ is linked automatically only in actual Debug configurations. ## Setup You need Git, CMake 3.28 or newer, Ninja, and a C++23 compiler and standard library -with `std::format` support. Install the selected modules' +with `std::format` support. The default root configuration also requires Python +3.11 or newer for documentation targets and hwloc 2.5+ (2.x) for worker support. +Set `CHERYL_BUILD_DOCS=OFF` or `CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC=OFF` to omit +the corresponding requirement. Install the selected modules' [system dependencies](docs/development/building.md#system-dependencies). ```sh @@ -42,7 +45,8 @@ Configure and build the demo from anywhere inside the checkout: ( set -e cd "$(git rev-parse --show-toplevel)" - cmake -S . -B build/release -G Ninja -DCMAKE_BUILD_TYPE=Release + cmake -S . -B build/release -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCHERYL_BUILD_DEMO=ON cmake --build build/release --target demo --parallel ./build/release/demo ) diff --git a/assets b/assets index 5603c197..98453b2a 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 5603c1974bff4054459edb23d36a52869cba863d +Subproject commit 98453b2a6e0edfcfba22eea3fde801526d72b861 diff --git a/cmake/CherylDependencies.cmake b/cmake/CherylDependencies.cmake index b1524c71..3a5ff0d6 100644 --- a/cmake/CherylDependencies.cmake +++ b/cmake/CherylDependencies.cmake @@ -32,9 +32,17 @@ if(NOT TARGET Backward::Interface) add_subdirectory("${CHERYL_REPOSITORY_ROOT}/extern/backward-cpp" extern/backward-cpp EXCLUDE_FROM_ALL) endif() if(NOT TARGET spdlog::spdlog) - set(SPDLOG_USE_STD_FORMAT ON) - add_subdirectory("${CHERYL_REPOSITORY_ROOT}/extern/spdlog" extern/spdlog EXCLUDE_FROM_ALL) + block(SCOPE_FOR VARIABLES) + set(SPDLOG_USE_STD_FORMAT ON) + if(CHERYL_BUILD_SHARED_LIBS) + set(BUILD_SHARED_LIBS ON) + set(SPDLOG_BUILD_SHARED ON) + endif() + add_subdirectory("${CHERYL_REPOSITORY_ROOT}/extern/spdlog" extern/spdlog EXCLUDE_FROM_ALL) + endblock() + cheryl_shared_dependency(spdlog::spdlog OWNED) endif() +cheryl_shared_dependency(spdlog::spdlog) if(NOT TARGET glm::glm-header-only) set(GLM_ENABLE_EXPERIMENTAL ON) add_subdirectory("${CHERYL_REPOSITORY_ROOT}/extern/glm" extern/glm EXCLUDE_FROM_ALL) diff --git a/cmake/CherylDocumentation.cmake b/cmake/CherylDocumentation.cmake index 97c7c07d..76a20f9b 100644 --- a/cmake/CherylDocumentation.cmake +++ b/cmake/CherylDocumentation.cmake @@ -1,4 +1,5 @@ include_guard(GLOBAL) +include("${CMAKE_CURRENT_LIST_DIR}/CherylOptions.cmake") function(cheryl_add_documentation_targets repository_root) if(TARGET docs) @@ -8,9 +9,7 @@ function(cheryl_add_documentation_targets repository_root) set(Python3_EXECUTABLE "${CHERYL_DOCS_PYTHON}") endif() find_package(Python3 3.11 REQUIRED COMPONENTS Interpreter) - set(CHERYL_DOCS_PYTHON "${Python3_EXECUTABLE}" CACHE FILEPATH "Python interpreter for documentation tool selection") - set(CHERYL_DOCS_CACHE_DIR "" CACHE PATH "Shared documentation tool cache; empty uses the user cache") - option(CHERYL_DOCS_FORCE_PINNED "Use locked documentation tools instead of compatible installed tools" OFF) + cheryl_configure_documentation_options("${Python3_EXECUTABLE}") set(tool_arguments) if(CHERYL_DOCS_CACHE_DIR) diff --git a/cmake/CherylLibraries.cmake b/cmake/CherylLibraries.cmake new file mode 100644 index 00000000..00b3d6a3 --- /dev/null +++ b/cmake/CherylLibraries.cmake @@ -0,0 +1,71 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/CherylOptions.cmake") +cheryl_configure_library_options() + +set(CHERYL_LIBRARY_TYPE STATIC) +if(CHERYL_BUILD_SHARED_LIBS) + set(CHERYL_LIBRARY_TYPE SHARED) + # Owned static dependencies may be incorporated into a Cheryl shared library. + set(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() + +function(cheryl_configure_library target component) + get_target_property(kind ${target} TYPE) + get_target_property(version ${target} VERSION) + get_target_property(output_name ${target} OUTPUT_NAME) + set_target_properties(${target} PROPERTIES + DEFINE_SYMBOL "CE_BUILDING_${component}" + LIBRARY_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}" + RUNTIME_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") + if(kind STREQUAL "STATIC_LIBRARY") + target_compile_definitions(${target} PUBLIC "CE_${component}_STATIC") + else() + set_target_properties(${target} PROPERTIES + CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN YES) + if(version) + string(REGEX MATCH "^[0-9]+" abi_version "${version}") + set_target_properties(${target} PROPERTIES SOVERSION "${abi_version}") + endif() + if(UNIX AND NOT APPLE) + set_target_properties(${target} PROPERTIES INSTALL_RPATH "$ORIGIN") + elseif(APPLE) + set_target_properties(${target} PROPERTIES INSTALL_RPATH "@loader_path") + endif() + if(NOT component STREQUAL "ENGINE" AND TARGET Cheryl::Engine) + get_target_property(engine_type Cheryl::Engine TYPE) + if(engine_type STREQUAL "STATIC_LIBRARY") + message(FATAL_ERROR "Shared Cheryl components require a shared Cheryl::Engine") + endif() + endif() + endif() + if(version AND output_name AND (kind STREQUAL "STATIC_LIBRARY" OR WIN32)) + set_target_properties(${target} PROPERTIES OUTPUT_NAME "${output_name}-${version}") + endif() +endfunction() + +function(cheryl_shared_dependency target) + cmake_parse_arguments(dependency "OWNED" "" "" ${ARGN}) + if(NOT CHERYL_BUILD_SHARED_LIBS) + return() + endif() + get_target_property(concrete ${target} ALIASED_TARGET) + if(NOT concrete) + set(concrete "${target}") + endif() + get_target_property(kind ${concrete} TYPE) + if(kind STREQUAL "STATIC_LIBRARY") + message(FATAL_ERROR "Shared Cheryl requires a shared ${target}; supply a shared dependency or let Cheryl build its source") + endif() + get_target_property(imported ${concrete} IMPORTED) + if(dependency_OWNED AND NOT imported AND kind STREQUAL "SHARED_LIBRARY") + set_target_properties(${concrete} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}" + LIBRARY_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") + if(UNIX AND NOT APPLE) + set_target_properties(${concrete} PROPERTIES INSTALL_RPATH "$ORIGIN") + elseif(APPLE) + set_target_properties(${concrete} PROPERTIES INSTALL_RPATH "@loader_path") + endif() + endif() +endfunction() diff --git a/cmake/CherylModules.cmake b/cmake/CherylModules.cmake index 4792046f..c2685da6 100644 --- a/cmake/CherylModules.cmake +++ b/cmake/CherylModules.cmake @@ -1,17 +1,22 @@ include("${CMAKE_CURRENT_LIST_DIR}/CherylVersions.cmake") include("${CMAKE_CURRENT_LIST_DIR}/CherylOptions.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/CherylLibraries.cmake") include("${CMAKE_CURRENT_LIST_DIR}/CherylOutputs.cmake") include("${CMAKE_CURRENT_LIST_DIR}/CherylLinkage.cmake") include("${CMAKE_CURRENT_LIST_DIR}/CherylPublicHeaders.cmake") include_guard(GLOBAL) # A module reuses supplied targets or bootstraps one explicitly selected checkout. -# Local variable scopes suppress recursive module/demo/test selection without +# Local variable scopes suppress unrelated build selections without # rewriting the host's cached options or compiling private engine source copies. function(cheryl_prepare_module) cmake_parse_arguments(module "REQUIRES_NATIVE" "" "" ${ARGN}) if(NOT TARGET Cheryl::Engine) block(SCOPE_FOR VARIABLES) + set(CHERYL_BUILD_DOCS OFF) + set(CHERYL_BUILD_SDK OFF) + set(CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC OFF) + set(CHERYL_BUILD_WORKER_TOPOLOGY_JSON OFF) set(CHERYL_BUILD_NATIVE_GLFW OFF) set(CHERYL_BUILD_DEBUG_TERMINAL_LINUX OFF) set(CHERYL_BUILD_OPENGL OFF) diff --git a/cmake/CherylOptions.cmake b/cmake/CherylOptions.cmake index 6d0bb3b2..62f8997c 100644 --- a/cmake/CherylOptions.cmake +++ b/cmake/CherylOptions.cmake @@ -11,6 +11,11 @@ function(cheryl_option name description default_value) return() endif() set_property(GLOBAL APPEND PROPERTY CHERYL_DECLARED_OPTIONS "${name}") + get_property(summary_scheduled GLOBAL PROPERTY CHERYL_OPTIONS_SUMMARY_SCHEDULED) + if(NOT summary_scheduled) + cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" CALL cheryl_print_options) + set_property(GLOBAL PROPERTY CHERYL_OPTIONS_SUMMARY_SCHEDULED TRUE) + endif() # Preserve the entry point's value through scoped Engine bootstrapping. set_property(GLOBAL PROPERTY "CHERYL_OPTION_${name}_VALUE" "${${name}}") set_property(GLOBAL PROPERTY "CHERYL_OPTION_${name}_REQUIRES" "${configured_REQUIRES}") @@ -72,9 +77,6 @@ function(cheryl_print_options) message(STATUS "----------------------------------") endfunction() -# Include options declared by modules and standalone entry points in one summary. -cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" CALL cheryl_print_options) - function(cheryl_check_option_requirements) get_property(declared_options GLOBAL PROPERTY CHERYL_DECLARED_OPTIONS) foreach(name IN LISTS declared_options) @@ -104,47 +106,110 @@ function(cheryl_check_option_requirements) endforeach() endfunction() +# Entry points declare only the groups they consume; all defaults live here. +function(cheryl_configure_module_options) + cheryl_option(CHERYL_BUILD_TESTS "Build module-local checks" OFF) +endfunction() + +function(cheryl_configure_native_options) + cheryl_option(CHERYL_NATIVE_INPUT "Include the Gainput-backed native input adapter" ON) + cheryl_option(CHERYL_NATIVE_NULL_PLATFORM "Use GLFW's null platform without X11 or Wayland" OFF) +endfunction() + +function(cheryl_configure_opengl_options) + set(CHERYL_NATIVE_GLFW_SOURCE "" CACHE PATH "Native GLFW module source directory when its target is not supplied") +endfunction() + +function(cheryl_configure_tgui_options) + set(CHERYL_TGUI_SOURCE "" CACHE PATH "TGUI 1.13.0 source directory; empty selects the pinned source or a supplied package") +endfunction() + +function(cheryl_configure_rmlui_options) + set(CHERYL_RMLUI_SOURCE "" CACHE PATH "RmlUi 6.3 source directory; empty selects the pinned source or a supplied package") +endfunction() + +function(cheryl_configure_rmlui_test_options) + set(CHERYL_RMLUI_TEST_FONT "" CACHE FILEPATH "RmlUi check font fixture; empty discovers an available TTF/OTF file") +endfunction() + +function(cheryl_configure_miniaudio_options) + set(CHERYL_MINIAUDIO_SOURCE "" CACHE PATH "Miniaudio 0.11.25 source directory, overriding the pinned submodule") +endfunction() + +function(cheryl_configure_debug_terminal_options) + set(CHERYL_DEBUG_TERMINAL_EMULATOR "auto" CACHE STRING "Linux native terminal emulator: auto, konsole, or xterm") + set_property(CACHE CHERYL_DEBUG_TERMINAL_EMULATOR PROPERTY STRINGS auto konsole xterm) + if(NOT CHERYL_DEBUG_TERMINAL_EMULATOR MATCHES "^(auto|konsole|xterm)$") + message(FATAL_ERROR "Unknown CHERYL_DEBUG_TERMINAL_EMULATOR: ${CHERYL_DEBUG_TERMINAL_EMULATOR}") + endif() +endfunction() + +function(cheryl_configure_logging_options) + set(CHERYL_LOG_PROFILE "auto" CACHE STRING "Logging defaults: auto, developer, support, or release") + set_property(CACHE CHERYL_LOG_PROFILE PROPERTY STRINGS auto developer support release) + set(CHERYL_LOG_COMPILED_MASK "" CACHE STRING "Optional severity-bit override (0x3f trace, 0x1f debug, 0x0f info, 0x07 warn, 0 off)") +endfunction() + +function(cheryl_configure_hwloc_options) + set(CHERYL_HWLOC_TARGET "" CACHE STRING "Supplied hwloc 2.5+ (2.x) SDK target") +endfunction() + +function(cheryl_configure_documentation_options interpreter) + set(CHERYL_DOCS_PYTHON "${interpreter}" CACHE FILEPATH "Python interpreter for documentation tool selection") + set(CHERYL_DOCS_CACHE_DIR "" CACHE PATH "Shared documentation tool cache; empty uses the user cache") + cheryl_option(CHERYL_DOCS_FORCE_PINNED "Use locked documentation tools instead of compatible installed tools" OFF) +endfunction() + +function(cheryl_configure_first_game_options) + set(CHERYL_FIRST_GAME_ENGINE_SOURCE "" CACHE PATH "Cheryl checkout for source consumption; empty uses an installed SDK") +endfunction() + +function(cheryl_configure_library_options) + cheryl_option(CHERYL_BUILD_SHARED_LIBS + "Build Cheryl Engine, modules and support as shared libraries" ON) +endfunction() + function(cheryl_configure_root_options) + cheryl_configure_library_options() cheryl_option(CHERYL_BUILD_DOCS - "Add documentation tooling and generation targets" OFF) + "Add documentation tooling and generation targets" ON) cheryl_option(CHERYL_BUILD_SDK - "Enable installation of the selected libraries, headers and dependencies" OFF) + "Enable installation of the selected libraries, headers and dependencies" ON) cheryl_option(WARN "Enable compiler warnings [-Wall]" OFF) cheryl_option(CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC - "Build optional hwloc worker topology and thread placement support" OFF) + "Build optional hwloc worker topology and thread placement support" ON) cheryl_option(CHERYL_BUILD_WORKER_TOPOLOGY_JSON - "Build optional worker configuration JSON loading support" OFF) + "Build optional worker configuration JSON loading support" ON) cheryl_option(CHERYL_BUILD_NATIVE_GLFW "Build the native GLFW window/input integration" ON) set(cheryl_terminal_linux_default OFF) - if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND + NOT CMAKE_CONFIGURATION_TYPES AND CMAKE_BUILD_TYPE STREQUAL "Debug") set(cheryl_terminal_linux_default ON) endif() cheryl_option(CHERYL_BUILD_DEBUG_TERMINAL_LINUX - "Select the Linux Debug terminal module for applications and test runners" ${cheryl_terminal_linux_default}) - cheryl_option(CHERYL_SANDBOX_BUILD - "Deprecated native GLFW null-platform selection without Gainput" OFF) + "Include the Linux output terminal in applications and test runners (defaults ON in supported Debug builds)" ${cheryl_terminal_linux_default}) cheryl_option(CHERYL_BUILD_OPENGL "Build the OpenGL backend" ON) cheryl_option(CHERYL_BUILD_UI_TGUI "Build the optional TGUI UI integration" ON) cheryl_option(CHERYL_BUILD_UI_RMLUI - "Build the optional RmlUi UI integration" OFF) + "Build the optional RmlUi UI integration" ON) cheryl_option(CHERYL_BUILD_AUDIO_MINIAUDIO - "Build the optional miniaudio playback integration" OFF) + "Build the optional miniaudio playback integration" ON) cheryl_option(CHERYL_BUILD_DEMO - "Build the windowed demonstration application" ON + "Build the windowed demonstration application" OFF REQUIRES CHERYL_BUILD_NATIVE_GLFW CHERYL_BUILD_OPENGL CHERYL_NATIVE_INPUT) cheryl_option(CHERYL_BUILD_FIRST_GAME "Build the maintained getting-started game" OFF REQUIRES CHERYL_BUILD_NATIVE_GLFW CHERYL_BUILD_OPENGL CHERYL_NATIVE_INPUT) cheryl_option(CHERYL_BUILD_TESTS - "Build Cheryl's tests and acceptance tools" ON) + "Build Cheryl's tests and acceptance tools" OFF) cheryl_option(CHERYL_BUILD_ALL_TESTS "Include owner and cross-project aggregate test runners in the default build" OFF REQUIRES CHERYL_BUILD_TESTS) @@ -159,17 +224,3 @@ function(cheryl_configure_root_options) message(FATAL_ERROR "Cheryl OpenGL requires Native GLFW; enable CHERYL_BUILD_NATIVE_GLFW or disable CHERYL_BUILD_OPENGL") endif() endfunction() - -function(cheryl_debug_terminal_configuration result) - set(CHERYL_DEBUG_TERMINAL "AUTO" CACHE STRING "Native output terminal inclusion: AUTO (Debug only), ON, or OFF") - set_property(CACHE CHERYL_DEBUG_TERMINAL PROPERTY STRINGS AUTO ON OFF) - if(CHERYL_DEBUG_TERMINAL STREQUAL "AUTO") - set(${result} "$" PARENT_SCOPE) - elseif(CHERYL_DEBUG_TERMINAL STREQUAL "ON") - set(${result} "1" PARENT_SCOPE) - elseif(CHERYL_DEBUG_TERMINAL STREQUAL "OFF") - set(${result} "0" PARENT_SCOPE) - else() - message(FATAL_ERROR "Unknown CHERYL_DEBUG_TERMINAL: ${CHERYL_DEBUG_TERMINAL}") - endif() -endfunction() diff --git a/cmake/CherylSdk.cmake b/cmake/CherylSdk.cmake index 66bf345a..93814386 100644 --- a/cmake/CherylSdk.cmake +++ b/cmake/CherylSdk.cmake @@ -134,7 +134,9 @@ if(NOT CHERYL_LOG_COMPILED_MASK STREQUAL "") endif() set_property(TARGET ${TARGET_LIB_ENGINE_S_LOGGING_CONFIG} PROPERTY INTERFACE_COMPILE_DEFINITIONS "$;$") -cheryl_sdk_target(${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS} SignalHandlers) +if(NOT CHERYL_BUILD_SHARED_LIBS) + cheryl_sdk_target(${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS} SignalHandlers) +endif() cheryl_sdk_target(${TARGET_LIB_ENGINE_S_STARTUP} Startup) cheryl_sdk_target(ctti DependencyCTTI) cheryl_sdk_target(spdlog DependencySPDLog) @@ -254,6 +256,34 @@ foreach(dependency IN ITEMS ctti spdlog backward-cpp glm cli11 glfw gainput glad endforeach() get_property(sdk_targets GLOBAL PROPERTY CHERYL_SDK_TARGETS) +set(sdk_component_versions "\"engine\": \"${VERSION_PROJECT_ENGINE}\", \"startup\": \"${VERSION_PROJECT_ENGINE}\"") +foreach(module IN ITEMS NATIVE_GLFW OPENGL UI_TGUI UI_RMLUI AUDIO_MINIAUDIO) + if(CHERYL_BUILD_${module}) + string(TOLOWER "${module}" component) + string(APPEND sdk_component_versions ", \"${component}\": \"${VERSION_PROJECT_MODULE_${module}}\"") + endif() +endforeach() +if(CHERYL_BUILD_OPENGL) + string(APPEND sdk_component_versions ", \"opengl_startup\": \"${VERSION_PROJECT_MODULE_OPENGL}\"") +endif() +foreach(provider IN ITEMS HWLOC JSON) + if(CHERYL_BUILD_WORKER_TOPOLOGY_${provider}) + string(TOLOWER "${provider}" component) + string(APPEND sdk_component_versions ", \"worker_topology_${component}\": \"${VERSION_PROJECT_ENGINE}\"") + endif() +endforeach() +get_target_property(sdk_engine_type ${TARGET_LIB_ENGINE} TYPE) +set(sdk_linkage static) +set(sdk_engine_directory "${CMAKE_INSTALL_LIBDIR}") +set(sdk_engine_import_library "") +if(sdk_engine_type STREQUAL "SHARED_LIBRARY") + set(sdk_linkage shared) + if(WIN32) + set(sdk_engine_directory "${CMAKE_INSTALL_BINDIR}") + set(sdk_engine_import_library "${CMAKE_INSTALL_LIBDIR}/$") + endif() +endif() +set(sdk_engine_library "${sdk_engine_directory}/$") add_custom_target(cheryl-sdk) add_dependencies(cheryl-sdk ${sdk_targets}) install(EXPORT CherylSdkTargets FILE CherylTargets.cmake NAMESPACE Cheryl:: @@ -263,6 +293,7 @@ configure_package_config_file("${CMAKE_CURRENT_LIST_DIR}/templates/CherylConfig. write_basic_package_version_file("${CHERYL_BUILD_ROOT}/CherylConfigVersion.cmake" VERSION ${VERSION_PROJECT_CHERYL} COMPATIBILITY SameMajorVersion) install(FILES "${CHERYL_BUILD_ROOT}/CherylConfig.cmake" "${CHERYL_BUILD_ROOT}/CherylConfigVersion.cmake" + "${CMAKE_CURRENT_LIST_DIR}/CherylOptions.cmake" DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Cheryl COMPONENT CherylSDK) set(sdk_msvc_runtime "") if(MSVC) @@ -276,4 +307,6 @@ if(MSVC) endif() endif() configure_file("${CMAKE_CURRENT_LIST_DIR}/templates/CherylSdk.json.in" - "${CHERYL_BUILD_ROOT}/cheryl-sdk.json" @ONLY) + "${CHERYL_BUILD_ROOT}/cheryl-sdk-configured.json" @ONLY) +file(GENERATE OUTPUT "${CHERYL_BUILD_ROOT}/cheryl-sdk.json" + INPUT "${CHERYL_BUILD_ROOT}/cheryl-sdk-configured.json") diff --git a/cmake/templates/CherylConfig.cmake.in b/cmake/templates/CherylConfig.cmake.in index c4890718..e7d13968 100644 --- a/cmake/templates/CherylConfig.cmake.in +++ b/cmake/templates/CherylConfig.cmake.in @@ -4,9 +4,17 @@ include(CMakeFindDependencyMacro) # Windows native dependencies are packaged in their own relocatable prefix. set(cheryl_sdk_prefix_path "${CMAKE_PREFIX_PATH}") set(cheryl_sdk_root "${PACKAGE_PREFIX_DIR}") +set(Cheryl_RUNTIME_DIRECTORIES) +if("@sdk_linkage@" STREQUAL "shared") + if(WIN32) + list(APPEND Cheryl_RUNTIME_DIRECTORIES "${cheryl_sdk_root}/@CMAKE_INSTALL_BINDIR@") + elseif(UNIX) + list(APPEND Cheryl_RUNTIME_DIRECTORIES "${cheryl_sdk_root}/@CMAKE_INSTALL_LIBDIR@") + endif() +endif() if(EXISTS "${cheryl_sdk_root}/dependencies") list(PREPEND CMAKE_PREFIX_PATH "${cheryl_sdk_root}/dependencies") - set(Cheryl_RUNTIME_DIRECTORIES "${cheryl_sdk_root}/dependencies/bin") + list(APPEND Cheryl_RUNTIME_DIRECTORIES "${cheryl_sdk_root}/dependencies/bin") endif() find_dependency(Threads) # HarfBuzz may import the FreeType config, so prefer its complete export set. diff --git a/cmake/templates/CherylSdk.json.in b/cmake/templates/CherylSdk.json.in index 9cf903b5..cb00a74d 100644 --- a/cmake/templates/CherylSdk.json.in +++ b/cmake/templates/CherylSdk.json.in @@ -1,5 +1,9 @@ { "version": "@VERSION_PROJECT_CHERYL@", + "component_versions": {@sdk_component_versions@}, + "linkage": "@sdk_linkage@", + "engine_library": "@sdk_engine_library@", + "engine_import_library": "@sdk_engine_import_library@", "system": "@CMAKE_SYSTEM_NAME@", "system_version": "@CMAKE_SYSTEM_VERSION@", "architecture": "@CMAKE_SYSTEM_PROCESSOR@", diff --git a/cmake/templates/Module.cmake.in b/cmake/templates/Module.cmake.in index 115e7618..48d99470 100644 --- a/cmake/templates/Module.cmake.in +++ b/cmake/templates/Module.cmake.in @@ -3,7 +3,7 @@ include("${CHERYL_REPOSITORY_ROOT}/cmake/CherylModules.cmake") if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) project(@MODULE_NAME@ VERSION ${VERSION_PROJECT_MODULE_@MODULE_ID@} LANGUAGES CXX) endif() -cheryl_option(CHERYL_BUILD_TESTS "Build module-local checks" OFF) +cheryl_configure_module_options() cheryl_prepare_module() file(GLOB_RECURSE module_sources CONFIGURE_DEPENDS diff --git a/docs/STATUS.md b/docs/STATUS.md index 65044e03..5e54069c 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -203,9 +203,9 @@ within each phase. | Pivot/anchor geometry | Implemented | Pivot presets, anchor conversion, quad/strip generation and Y-up geometry. | [anchor.h](../projects/engine/include/cheryl/math/anchor.h) | | General math utilities | Implemented | Byte, pointer, binary, fitting, numeric and time helper functions. | [math/](../projects/engine/include/cheryl/math/) | | File manager and path I/O | Implemented | Core file-manager interface and path-oriented file operations, distinct from installed font discovery. | [file-mgr.h](../projects/engine/include/cheryl/core/resources/fileio/file-mgr.h) | -| External Debug terminal | Implemented · Linux desktop acceptance pending | Optional Linux module owns process capture/viewer and test-report routing. The configuration matrix is accepted; AUTO links actual Debug only and non-Debug inclusion needs a runtime enable option. Engine supplies an optional startup callback attachment. | [module guide](../projects/modules/platform/debug-terminal-linux/README.md), [desktop QA](testing-requests.md#tr18-qa-linux-debug-terminal-on-a-real-desktop) | +| External Debug terminal | Implemented · Linux desktop acceptance pending | Optional Linux module owns process capture/viewer and test-report routing. Selection defaults on for supported single-configuration Debug builds; explicit ON includes every configuration, with non-Debug startup requiring a runtime enable option. Engine supplies an optional startup callback attachment. | [module guide](../projects/modules/platform/debug-terminal-linux/README.md), [desktop QA](testing-requests.md#tr18-qa-linux-debug-terminal-on-a-real-desktop) | | **Build and verification** | | | | -| Installed CMake package | Implemented | Opt-in CherylSDK component exports prebuilt selected libraries, headers, dependency targets and licenses; selected hosted Linux and Windows installed consumers are accepted. Windows bundles native dependencies. | [SDK consumption](development/consuming-engine.md#prebuilt-development-sdks) | +| Installed CMake package | Implemented | CherylSDK component exports prebuilt selected libraries, headers, dependency targets and licenses; selected hosted Linux and Windows installed consumers are accepted. Windows bundles native dependencies. | [SDK consumption](development/consuming-engine.md#prebuilt-development-sdks) | | GitHub CI and SDK artifacts | Implemented | Linux GCC/Clang and Windows MSVC focused builds/tests plus relocated SDK consumers; successful main validation collects compiled development SDK artifacts without applications or release publication. Hosted builds and relocated consumers are accepted with explicit Windows skipped-case limits; first main-push artifact collection remains pending. | [CI matrix](development/building.md#github-ci-and-sdk-artifacts), [TR20](testing-requests.md#tr20-automated-main-push-sdk-collection) | | Modular CMake root | Implemented | C++23 build splits Engine and native graphics/platform/UI/audio owners. | [CMakeLists.txt](../CMakeLists.txt) | | CMake option selection | Implemented | Root switches for GLFW, OpenGL, TGUI, RmlUi, miniaudio, native input, demo and test modes. | [CherylOptions.cmake](../cmake/CherylOptions.cmake) | diff --git a/docs/development/architecture-validation.md b/docs/development/architecture-validation.md index ab439c27..cf9c1699 100644 --- a/docs/development/architecture-validation.md +++ b/docs/development/architecture-validation.md @@ -43,7 +43,7 @@ registration. ### Engine only -A fresh Release configuration provides a neutral graph without sandbox mode: +A fresh Release configuration provides a neutral graph: ```sh ( @@ -58,9 +58,11 @@ A fresh Release configuration provides a neutral graph without sandbox mode: -DCHERYL_BUILD_DEBUG_TERMINAL_LINUX=OFF \ -DCHERYL_BUILD_UI_TGUI=OFF -DCHERYL_BUILD_UI_RMLUI=OFF \ -DCHERYL_BUILD_AUDIO_MINIAUDIO=OFF \ + -DCHERYL_BUILD_WORKER_TOPOLOGY_HWLOC=OFF -DCHERYL_BUILD_WORKER_TOPOLOGY_JSON=OFF \ + -DCHERYL_BUILD_DOCS=OFF -DCHERYL_BUILD_SDK=OFF \ -DCHERYL_BUILD_TESTS=ON -DCHERYL_BUILD_CONSUMER_TESTS=ON \ -DCHERYL_BUILD_ALL_TESTS=OFF -DCHERYL_BUILD_ACCEPTANCE_TESTS=OFF \ - -DCHERYL_BUILD_DEMO=OFF -DCHERYL_SANDBOX_BUILD=OFF + -DCHERYL_BUILD_DEMO=OFF cmake --build build-validation-release --parallel --target \ consumer-cengine tests-engine tests-logging \ acceptance-logging acceptance-signal @@ -130,11 +132,13 @@ caller's working directory: -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_SCAN_FOR_MODULES=OFF \ -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST \ - -DCHERYL_LOG_PROFILE=developer -DCHERYL_SANDBOX_BUILD=OFF \ + -DCHERYL_LOG_PROFILE=developer \ -DCHERYL_BUILD_NATIVE_GLFW=OFF -DCHERYL_BUILD_OPENGL=OFF \ -DCHERYL_BUILD_DEBUG_TERMINAL_LINUX=OFF \ -DCHERYL_BUILD_UI_TGUI=OFF -DCHERYL_BUILD_UI_RMLUI=OFF \ -DCHERYL_BUILD_AUDIO_MINIAUDIO=OFF \ + -DCHERYL_BUILD_WORKER_TOPOLOGY_HWLOC=OFF -DCHERYL_BUILD_WORKER_TOPOLOGY_JSON=OFF \ + -DCHERYL_BUILD_DOCS=OFF -DCHERYL_BUILD_SDK=OFF \ -DCHERYL_BUILD_TESTS=ON -DCHERYL_BUILD_CONSUMER_TESTS=ON \ -DCHERYL_BUILD_ALL_TESTS=OFF -DCHERYL_BUILD_ACCEPTANCE_TESTS=ON \ -DCHERYL_BUILD_DEMO=OFF @@ -320,9 +324,15 @@ enable_testing() set(CHERYL_BUILD_ROOT "${CMAKE_CURRENT_BINARY_DIR}") block(SCOPE_FOR VARIABLES) 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) + set(CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC OFF) + set(CHERYL_BUILD_WORKER_TOPOLOGY_JSON OFF) + set(CHERYL_BUILD_DOCS OFF) + set(CHERYL_BUILD_SDK OFF) set(CHERYL_BUILD_TESTS OFF) set(CHERYL_BUILD_CONSUMER_TESTS OFF) set(CHERYL_BUILD_DEMO OFF) diff --git a/docs/development/building.md b/docs/development/building.md index 35a84d99..f187b409 100644 --- a/docs/development/building.md +++ b/docs/development/building.md @@ -16,7 +16,7 @@ source directories. SDK installation collects them into one redistributable | Artifact | Trigger | Result | | --- | --- | --- | | HTML manual and C++ reference | Run `./generate-docs.sh`, or build `docs` in the docs-only project below; `--clean` / `docs-clean` forces full regeneration. Enable `CHERYL_BUILD_DOCS` to add these targets to an engine build. | `docs/generation/generated/html/index.html` and linked pages. | -| Engine library | Build `cengine`; see [engine-only configuration](#engine-only-configuration). | `libcheryl-engine.a` on Linux or `cheryl-engine.lib` on Windows in the build root, plus required dependency artifacts. | +| Engine library | Build `cengine`; see [engine-only configuration](#engine-only-configuration). | Versioned Engine library in the build root, plus required dependency artifacts; see [library linkage](#library-linkage). | | Selected SDK libraries | Enable `CHERYL_BUILD_SDK`, then build `cheryl-sdk`. | Engine, selected modules/support and their owned dependencies in the build tree. | | Redistributable SDK directory | Build `cheryl-sdk`, then install the `CherylSDK` component. | One prefix containing `include/`, libraries, CMake package files and licenses. | | SDK archive with manifest/checksum | Run `.github/ci/package.py` after building `cheryl-sdk`. | Linux `.tar.gz` or Windows `.zip`, `SDK-MANIFEST.json` and `SHA256SUMS`. | @@ -57,34 +57,34 @@ tools automatically. ## Configuration options Pass settings as `-DNAME=value`. These defaults apply to a fresh root configuration; -existing build directories and IDE profiles retain their cache. Native options -are declared only when Native GLFW is selected; the RmlUi verification option is -declared only when RmlUi is selected. +existing build directories and IDE profiles retain their cache. All Cheryl-owned +option definitions and defaults live in +[`cmake/CherylOptions.cmake`](../../cmake/CherylOptions.cmake). Component and +standalone entry points select which options to declare; optional backend and +dependency-source settings may appear only when their component is selected. | Option | Default | Effect | | --- | --- | --- | | `CHERYL_BUILD_NATIVE_GLFW` | `ON` | Selects GLFW display/windows and optional input. | -| `CHERYL_BUILD_DEBUG_TERMINAL_LINUX` | Linux GNU/Clang: `ON`; elsewhere: `OFF` | Selects the optional Linux output-terminal module for root applications/test runners. | -| `CHERYL_DEBUG_TERMINAL` | `AUTO` | Links the selected terminal only in Debug; `ON` includes other configurations, where display requires `--debug-terminal`; `OFF` omits it. | +| `CHERYL_BUILD_DEBUG_TERMINAL_LINUX` | Single-configuration Linux GNU/Clang with `CMAKE_BUILD_TYPE=Debug`: `ON`; otherwise: `OFF` | Includes the optional Linux output terminal in consuming applications/test runners. Explicit `ON` includes it in every configuration built from that directory. | | `CHERYL_DEBUG_TERMINAL_EMULATOR` | `auto` | Linux module launcher selection: `auto`, `konsole` or `xterm`. | | `CHERYL_BUILD_OPENGL` | `ON` | Selects the complete OpenGL backend; requires Native GLFW. | | `CHERYL_BUILD_UI_TGUI` | `ON` | Selects TGUI independently of platform/graphics. | -| `CHERYL_BUILD_UI_RMLUI` | `OFF` | Selects RmlUi independently; can coexist with TGUI. | -| `CHERYL_RMLUI_PLACEHOLDER_FIX_VERIFIED` | `OFF` | Attests that a supplied RmlUi library includes the placeholder hit-testing correction. Set only after verification; Cheryl-owned source builds declare their correction automatically. | -| `CHERYL_BUILD_AUDIO_MINIAUDIO` | `OFF` | Selects miniaudio decoding, output and streaming. | -| `CHERYL_BUILD_DEMO` | `ON` | Adds `demo` with Native GLFW, OpenGL and native input selected. | +| `CHERYL_BUILD_UI_RMLUI` | `ON` | Selects RmlUi independently; can coexist with TGUI. | +| `CHERYL_BUILD_AUDIO_MINIAUDIO` | `ON` | Selects miniaudio decoding, output and streaming. | +| `CHERYL_BUILD_DEMO` | `OFF` | Adds `demo` with Native GLFW, OpenGL and native input selected. | | `CHERYL_BUILD_FIRST_GAME` | `OFF` | Adds the maintained `first-game` walkthrough application with Native GLFW, OpenGL and native input selected. | -| `CHERYL_BUILD_DOCS` | `OFF` | Adds the optional `docs-tools`, `docs` and `docs-clean` targets; tools are selected when these targets run. | -| `CHERYL_BUILD_TESTS` | `ON` | Adds focused tests and explicitly buildable acceptance/aggregate runners. | +| `CHERYL_BUILD_DOCS` | `ON` | Adds the optional `docs-tools`, `docs` and `docs-clean` targets; tools are selected when these targets run. | +| `CHERYL_BUILD_TESTS` | `OFF` | Adds focused tests and explicitly buildable acceptance/aggregate runners. | | `CHERYL_BUILD_ALL_TESTS` | `OFF` | Includes aggregates in the default build and CTest; requires tests. | | `CHERYL_BUILD_ACCEPTANCE_TESTS` | `OFF` | Includes acceptance runners in the default build; requires tests. | | `CHERYL_BUILD_CONSUMER_TESTS` | `OFF` | Adds selected owners' consumers and first-include probes independently of GoogleTest. | -| `CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC` | `OFF` | Selects optional runtime topology discovery and thread binding through hwloc. | -| `CHERYL_BUILD_WORKER_TOPOLOGY_JSON` | `OFF` | Selects the versioned worker settings loader independently of hwloc. | -| `CHERYL_BUILD_SDK` | `OFF` | Adds the `cheryl-sdk` build target and `CherylSDK` install/export component for selected libraries, headers and dependencies. | +| `CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC` | `ON` | Selects optional runtime topology discovery and thread binding through hwloc. | +| `CHERYL_BUILD_WORKER_TOPOLOGY_JSON` | `ON` | Selects the versioned worker settings loader independently of hwloc. | +| `CHERYL_BUILD_SDK` | `ON` | Adds the `cheryl-sdk` build target and `CherylSDK` install/export component for selected libraries, headers and dependencies. | +| `CHERYL_BUILD_SHARED_LIBS` | `ON` | Builds Cheryl Engine, compiled modules and support as shared libraries, with matching owned source dependencies. | | `CHERYL_NATIVE_INPUT` | `ON` | Includes Gainput-backed input in Native GLFW. | | `CHERYL_NATIVE_NULL_PLATFORM` | `OFF` | Configures an owned GLFW without X11/Wayland. Disable native input to omit Gainput/X11. | -| `CHERYL_SANDBOX_BUILD` | `OFF` | Deprecated shorthand defaulting native input off and the null platform on. Prefer explicit options. | | `CHERYL_LOG_PROFILE` | `auto` | Selects `developer`, `support` or `release`; `auto` follows the build configuration. | | `CHERYL_LOG_COMPILED_MASK` | Empty | Overrides compiled severity bits (`0`–`63`); see [logging policy](../runtime/logging.md#compile-policy). | | `WARN` | `OFF` | Adds `-Wall`. | @@ -105,7 +105,8 @@ substitute `RelWithDebInfo`/`build/support` or `Release`/`build/release` as need ( set -e cd "$(git rev-parse --show-toplevel)" - cmake -S . -B build/debug -G Ninja -DCMAKE_BUILD_TYPE=Debug + cmake -S . -B build/debug -G Ninja -DCMAKE_BUILD_TYPE=Debug \ + -DCHERYL_BUILD_DEMO=ON cmake --build build/debug --target demo --parallel ) ``` @@ -122,6 +123,28 @@ Target and artifact declarations are centralized in [CherylTargets.cmake](../../cmake/CherylTargets.cmake) and [CherylOutputs.cmake](../../cmake/CherylOutputs.cmake). +### Library linkage + +Cheryl Engine, modules and support default to shared libraries. Set +`-DCHERYL_BUILD_SHARED_LIBS=OFF` during configuration for static libraries; +the same public CMake targets work with either selection. `BUILD_SHARED_LIBS` alone +does not select Cheryl's linkage. Keep static/shared builds and SDK prefixes separate. Supplied dependency +targets retain their settings and must satisfy the selected shared-library requirements. +The Linux terminal's `main` wrapper remains static; its startup support follows +the selected Cheryl linkage. + +Artifact names use the component versions in +[CherylVersions.cmake](../../cmake/CherylVersions.cmake). For Engine 4.0.0: + +| Platform | Static | Shared | +| --- | --- | --- | +| Linux | `libcheryl-engine-4.0.0.a` | `libcheryl-engine.so.4.0.0`, with SONAME/linker symlinks. | +| Windows/MSVC | `cheryl-engine-4.0.0.lib` | `cheryl-engine-4.0.0.dll` and its `.lib` import library. | + +Link CMake targets to obtain the correct artifact and import/export definitions. +Shared applications also need the matching libraries at runtime; see +[SDK runtime paths](consuming-engine.md#prebuilt-development-sdks). + ### Engine-only configuration This omits GLFW, Gainput, OpenGL, GLAD, UI toolkits and miniaudio. Engine still needs @@ -137,7 +160,8 @@ its text dependencies: -DCHERYL_BUILD_UI_TGUI=OFF -DCHERYL_BUILD_UI_RMLUI=OFF \ -DCHERYL_BUILD_AUDIO_MINIAUDIO=OFF \ -DCHERYL_BUILD_WORKER_TOPOLOGY_HWLOC=OFF -DCHERYL_BUILD_WORKER_TOPOLOGY_JSON=OFF \ - -DCHERYL_BUILD_DEMO=OFF -DCHERYL_BUILD_TESTS=OFF + -DCHERYL_BUILD_DEMO=OFF -DCHERYL_BUILD_TESTS=OFF \ + -DCHERYL_BUILD_DOCS=OFF -DCHERYL_BUILD_SDK=OFF cmake --build build/engine-only --target cengine --parallel ) ``` @@ -172,9 +196,17 @@ Engine/OpenGL targets do not acquire CLI11 usage requirements. See [application startup](consuming-engine.md#command-line-startup). The terminal module adds a separate optional startup target and linker entry hook; -plain Engine links neither. Debug display requires a desktop and supported emulator; -missing prerequisites fall back to inherited output. Headless Debug and test -discovery create no viewer. See the +plain Engine links neither. Its default is calculated when the option first enters +the CMake cache; changing `CMAKE_BUILD_TYPE` later retains the cached selection. +Set `CHERYL_BUILD_DEBUG_TERMINAL_LINUX=ON` or `OFF` explicitly when changing it. +Multi-configuration generators default to `OFF`; explicit `ON` includes the terminal +in every configuration. Standalone terminal-module consumers also set this option +explicitly. + +Included Debug builds display automatically; other configurations require +`--debug-terminal`. `--no-debug-terminal` disables display. Display requires a +desktop and supported emulator; missing prerequisites fall back to inherited output. +Headless Debug and test discovery create no viewer. See the [Linux terminal guide](../../projects/modules/platform/debug-terminal-linux/README.md) for output/report separation and lifetime policy. `ON` is a build inclusion option, not a request to display a Release window automatically. @@ -187,7 +219,8 @@ Package names vary by operating system. | Requirement | Selection | Notes | | --- | --- | --- | | Threads | Engine | Discovered through `Threads::Threads`. | -| hwloc 2.5+ (2.x) | WorkerTopology::Hwloc only | Runtime CPU/core/cache discovery and calling-thread binding; omitted configurations do not discover/download/link it. | +| Python 3.11+ | Documentation targets (enabled by default) | Required during configuration; Doxygen/Sphinx are selected when documentation targets run. | +| hwloc 2.5+ (2.x) | WorkerTopology::Hwloc (selected by default) | Runtime CPU/core/cache discovery and calling-thread binding; omitted configurations do not discover/download/link it. | | FreeType, HarfBuzz, ICU uc/i18n | Engine | Font inspection/rasterization, shaping, bidi and boundaries; required with UI disabled too. | | X11 and extensions | GLFW X11 / native Linux input | GLFW checks XRandR, Xinerama, XKB, Xcursor, XInput and X Shape headers. Gainput input also needs X11. | | Wayland, xkbcommon, `wayland-scanner` | Bundled GLFW Wayland | Linux GLFW enables X11 and Wayland by default. Disable Wayland for an X11-only build. | @@ -212,7 +245,7 @@ Integration SDKs belong to their modules; GoogleTest is test-only. See | Python cannot import `jinja2` | Install it in the selected interpreter or set `Python_EXECUTABLE`. | | GLFW cannot find Wayland tools/libraries | Install the requirements or set `GLFW_BUILD_WAYLAND=OFF`. | | CMake 4 rejects an old dependency policy | Set `CMAKE_POLICY_VERSION_MINIMUM=3.5`. | -| Supplied RmlUi Core lacks the placeholder-correction declaration | Use Cheryl-owned source or provide verified target/configuration metadata according to the [dependency contract](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract). | +| Supplied RmlUi Core lacks the placeholder-correction declaration | Use Cheryl-owned source or declare the correction on the verified dependency's Core target according to the [dependency contract](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract). | | Standard C++ headers cannot find `CE::Math.h` or `stdlib.h` | Inspect stale compiler discovery before editing includes; see [recovery](consuming-engine.md#standard-headers-in-an-existing-build). | Successful configuration/build does not establish native platform coverage. The @@ -227,9 +260,9 @@ the same targets without graphics or native development dependencies. `CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC=ON` adds `cengine_worker_topology_hwloc` (`Cheryl::WorkerTopology::Hwloc`); JSON selection adds `cengine_worker_topology_json` -(`Cheryl::WorkerTopology::Json`). Both options default off. A plain Engine consumer -needs neither support library. Selecting both also registers the explicitly -buildable `example_worker_topology` shared-pool composition example. +(`Cheryl::WorkerTopology::Json`). Both options default to `ON`; a plain Engine +consumer can disable both because it needs neither support library. Selecting both +also registers the explicitly buildable `example_worker_topology` shared-pool composition example. The [worker guide](../runtime/worker-execution.md) owns provider lifetime, placement policies, capacity and configuration semantics. @@ -244,7 +277,7 @@ CPU vendor is not a CMake selection. ## Development SDK builds -SDK export is opt-in and requires Cheryl-owned builds of the pinned source +SDK export is enabled by default and requires Cheryl-owned builds of the pinned source dependencies; imported dependency targets are rejected by this packaging path, except an imported hidapi package that remains external to the SDK. Ordinary source composition can still reuse supplied targets. Use a clean checkout @@ -252,8 +285,8 @@ with submodules at their recorded revisions for reproducible archives. The packa records the source commit and verifies recursive submodule HEADs against its gitlinks. Use a single-configuration Ninja build and select the modules/helpers to distribute. -On Linux, this builds the default Native GLFW/OpenGL/TGUI libraries and common -Startup support without building the demo or tests: +On Linux, this builds the default modules and worker support, plus common Startup +support, without building the demo or tests: ```sh ( @@ -310,6 +343,9 @@ native dependency prefix; use `package.py` for the complete Windows bundle. Archive names include the Cheryl version from [CherylVersions.cmake](../../cmake/CherylVersions.cmake), platform/distribution, architecture, compiler/version, configuration and commit ID. +They also include `static` or `shared`; `SDK-MANIFEST.json` records that linkage, +enabled component versions and the exact Engine artifact paths. Linux archives +preserve relative SONAME symlinks within the SDK; broken or escaping links are rejected. Windows CI builds through the pinned vcpkg manifest in [.github/ci](../../.github/ci/vcpkg.json) with the `x64-windows` triplet. Packaging copies release headers, import libraries, @@ -336,6 +372,8 @@ vcpkg baseline. Builds use Ninja and available CPU parallelism. | `windows-msvc-hwloc` | Windows 2025 x64, MSVC RelWithDebInfo | Headless Engine and hwloc/JSON with vcpkg native libraries. | | `linux-modules` | Ubuntu 24.04 x64, GCC 14 Release | Native GLFW/input, OpenGL, TGUI, RmlUi, miniaudio, hwloc/JSON and SDK. | | `windows-modules` | Windows 2025 x64, MSVC RelWithDebInfo | The same selected modules/helpers and SDK, with bundled native dependencies. | +| `linux-modules-shared` | Ubuntu 24.04 x64, GCC 14 Release | The modules/SDK composition with shared Cheryl libraries and owned dependencies. | +| `windows-modules-shared` | Windows 2025 x64, MSVC RelWithDebInfo | The shared modules/SDK composition, including DLL runtime discovery. | [run.py](../../.github/ci/run.py) configures explicit owners/options/build targets, runs each focused owner prefix once and executes independent consumers. Modules @@ -360,8 +398,7 @@ for 14 days. These are workflow artifacts; the workflow creates no GitHub releas uploads no release assets and publishes no demo package. Release publication is a separate future decision. -The selected hosted Ubuntu GCC/Clang and Windows MSVC producer jobs and relocated -SDK consumers are accepted. The first main-push artifact collection remains +The first main-push artifact collection remains [TR20](../testing-requests.md#tr20-automated-main-push-sdk-collection); collection is intentionally skipped on PR, `develop` and manual runs. diff --git a/docs/development/consuming-engine.md b/docs/development/consuming-engine.md index fbd21517..bc17a1b1 100644 --- a/docs/development/consuming-engine.md +++ b/docs/development/consuming-engine.md @@ -17,13 +17,13 @@ That target combines Engine, configured modules and available support libraries. Its include directories and feature definitions make the matching headers visible to the compiler and the IDE. The same include works with source and SDK targets. Individual targets remain available when the application needs a smaller dependency -set; `Cheryl::Engine` is the neutral library backed by build target `cengine` and -archive output name `cheryl-engine`. +set; `Cheryl::Engine` is the neutral library backed by build target `cengine`. +Its artifact uses the Engine component version; CMake selects the matching filename. ## Source checkout A relative path to the Cheryl checkout through `add_subdirectory` is supported. -For example, with the root's default Native GLFW/OpenGL/TGUI selections: +For example, with the root's default module and worker-support selections: ```cmake cmake_minimum_required(VERSION 3.28) @@ -51,6 +51,35 @@ The source facade also links the configured Linux Debug terminal and its entry attachment when that module is enabled. The example disables it; use the granular targets below when you want to choose support and diagnostic behavior individually. +### Windows source-build runtime + +Shared libraries must be available when the game starts. With +`add_subdirectory(... cheryl)` and a single-configuration Ninja build in +`build/release`, Cheryl writes its DLLs to `build/release/cheryl`; the game's +executable can be in the parent directory. Multi-configuration generators add a +configuration subdirectory, such as `cheryl/Release`. + +After creating and linking `my_game`, copy CMake-known runtime DLLs beside it: + +```cmake +get_target_property(cheryl_engine_type Cheryl::Engine TYPE) +if(WIN32 AND cheryl_engine_type STREQUAL "SHARED_LIBRARY") + add_custom_command(TARGET my_game POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy -t "$" + "$" + COMMAND_EXPAND_LISTS + VERBATIM) +endif() +``` + +This stages Cheryl and dependency DLLs whose runtime locations are known to CMake. +Native FreeType, HarfBuzz or ICU packages can still require their SDK's DLL +directories on the process `PATH`: CMake's +[`TARGET_RUNTIME_DLLS`](https://cmake.org/cmake/help/v3.28/manual/cmake-generator-expressions.7.html#genex:TARGET_RUNTIME_DLLS) +cannot list DLLs for imported targets without runtime-location metadata. +Alternatively, configure the IDE/launcher `PATH` with both Cheryl's configuration +output directory and the native SDK's DLL directories instead of copying. + ### Granular composition Link selected targets individually to control the application's dependency set. @@ -65,6 +94,10 @@ set(CHERYL_BUILD_OPENGL OFF) set(CHERYL_BUILD_UI_TGUI OFF) set(CHERYL_BUILD_UI_RMLUI OFF) set(CHERYL_BUILD_AUDIO_MINIAUDIO OFF) +set(CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC OFF) +set(CHERYL_BUILD_WORKER_TOPOLOGY_JSON OFF) +set(CHERYL_BUILD_DOCS OFF) +set(CHERYL_BUILD_SDK OFF) add_subdirectory(path/to/cheryl-engine cheryl) add_executable(application main.cpp) target_link_libraries(application PRIVATE Cheryl::Engine) @@ -107,7 +140,7 @@ For a Linux Release SDK, configure the application's directory as follows: ``` The SDK contains compiled Engine, Startup and selected module/topology libraries, -their dependency libraries or headers, the crash-bootstrap object, public headers, +their dependency libraries or headers, the Engine crash bootstrap, 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. @@ -118,7 +151,8 @@ Its `Cheryl::Cheryl` target selects the components distributed in that SDK; [below](#dependencies-and-headers). The [build guide](building.md#development-sdk-builds) describes production; `SDK-MANIFEST.json` records selected features, source/submodule revisions, OS, -architecture, compiler/version, configuration, logging profile and native versions. +architecture, compiler/version, configuration, logging profile, native versions, +enabled component versions and static/shared linkage. Use a package compatible with the target OS/distribution, architecture, compiler, standard-library ABI and configuration. Windows CI SDKs use MSVC x64 RelWithDebInfo @@ -128,10 +162,24 @@ 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. +Use Engine and modules from the same matched SDK/binary set. Shared builds give +built-in Engine/Native GLFW singletons, cache/provider state and logging state one +owning library. Application-defined `CE::State::Singleton` and generic allocator +template statics can still have separate instances in different DLLs. Share such +application state through an explicit owner. + +Retained object/resource handles keep their captured release contexts; release +through their deleters. For manual destruction, call +`CE::Memory::Objects::Constructor::destroy(...)` in the allocating library, or +retain and use the original construction context. A direct static call from another +DLL can consult a different tracker. Keep the owning libraries loaded while their +objects and deleters remain in use. Plugin hot unloading and arbitrary module-version +mixing are outside this SDK contract. + | 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. | +| Windows | The `dependencies/` prefix supplies pinned vcpkg headers, import libraries, CMake files, release DLLs and licenses. Keep it with the SDK; shared Cheryl DLLs also live in the SDK's `bin/`. Both DLL directories must be on the application's 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. @@ -140,9 +188,13 @@ headers and licenses are packaged. Linux consumers also need pkg-config and the backend's libudev/libusb development libraries, plus Iconv when selected; the package config discovers only the dependencies that backend requires. Packaging does not resolve the [Linux controller limitations](../../projects/modules/platform/native-glfw/README.md#controller-backend-limits). -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. +The package exposes `Cheryl_RUNTIME_DIRECTORIES`: shared SDKs use `lib/` on Linux +and `bin/` on Windows; Windows bundles also include `dependencies/bin/`. +Use those paths for application runtime setup or deploy the required libraries +beside your executable. Linux shared libraries find their packaged neighbors +through an origin-relative runtime path. Windows config also prepends the bundled +dependency prefix for CMake discovery; it does not deploy DLLs 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 @@ -354,17 +406,20 @@ 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 +`Cheryl::Engine` delivers the automatic crash bootstrap. Static builds forward +its object directly to final consumers, so the linker cannot omit an unreferenced +archive initializer. Shared Engine embeds one copy; modules and applications +reuse that copy through Engine. 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 +capture/fallback implementation in all builds. Static SDKs also export +`Cheryl::SignalHandlers`; shared SDKs keep the bootstrap inside `Cheryl::Engine`. +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`; Debug shared +Engine exports its owned instance, and static Debug consumers receive it through +the bootstrap object. The definition does not change installation scope. See the [signal procedure](architecture-validation.md#manual-acceptance-drivers). ## Independent consumers and header probes diff --git a/docs/development/logging-acceptance.md b/docs/development/logging-acceptance.md index 295d1bbe..21fbe106 100644 --- a/docs/development/logging-acceptance.md +++ b/docs/development/logging-acceptance.md @@ -31,8 +31,10 @@ For the developer row: -DCHERYL_BUILD_DEBUG_TERMINAL_LINUX=OFF \ -DCHERYL_BUILD_UI_TGUI=OFF -DCHERYL_BUILD_UI_RMLUI=OFF \ -DCHERYL_BUILD_AUDIO_MINIAUDIO=OFF \ + -DCHERYL_BUILD_WORKER_TOPOLOGY_HWLOC=OFF -DCHERYL_BUILD_WORKER_TOPOLOGY_JSON=OFF \ + -DCHERYL_BUILD_DOCS=OFF -DCHERYL_BUILD_SDK=OFF \ -DCHERYL_BUILD_DEMO=OFF \ - -DCHERYL_SANDBOX_BUILD=OFF -DCHERYL_LOG_PROFILE=developer + -DCHERYL_LOG_PROFILE=developer cmake --build build/log-developer --parallel \ --target tests-logging acceptance-logging python3 projects/engine/tests/logging-acceptance/logging.py build/log-developer diff --git a/docs/development/modules.md b/docs/development/modules.md index 645c4f39..196d5c31 100644 --- a/docs/development/modules.md +++ b/docs/development/modules.md @@ -72,10 +72,11 @@ because they have few callers. ## Select an assembly -The root defaults to Native GLFW, OpenGL, TGUI and the demo. RmlUi and miniaudio -are optional and default to `OFF`. UI and audio selection is independent of -platform/graphics selection. OpenGL requires Native GLFW; the demo also requires -native input. Disable demo and every module option for Engine alone. +The root defaults to Native GLFW, OpenGL, TGUI, RmlUi and miniaudio; the demo is +opt-in. UI and audio selection is independent of platform/graphics selection. +OpenGL requires Native GLFW; the demo also requires native input. For Engine alone, +disable every module and both worker-support options as shown in the +[engine-only configuration](building.md#engine-only-configuration). On Linux GNU/Clang this includes `CHERYL_BUILD_DEBUG_TERMINAL_LINUX=OFF`. The root automatically links the selected terminal interface to its demo/shared test entry point and the source `Cheryl::Cheryl` facade. External applications can @@ -116,9 +117,8 @@ granular owner headers, such as `cheryl/backends/native-glfw.h`. `CHERYL_NATIVE_INPUT=OFF` omits Gainput and the default-input factory overload. `CHERYL_NATIVE_NULL_PLATFORM=ON` configures an owned GLFW without X11/Wayland; -a supplied GLFW target keeps its host settings. Prefer these explicit options to -the deprecated sandbox shorthand. The [build guide](building.md) owns their -configuration and dependency details. +a supplied GLFW target keeps its host settings. The [build guide](building.md) +owns their configuration and dependency details. Selection preserves owner lifetimes: input detaches while its window is live, resources retire before context destruction, and runtime shutdown settles workers diff --git a/docs/development/native-desktop-checks.md b/docs/development/native-desktop-checks.md index 85740a52..278d101d 100644 --- a/docs/development/native-desktop-checks.md +++ b/docs/development/native-desktop-checks.md @@ -87,10 +87,10 @@ the printed result. Physical observations use the following controller checks. cd "$(git rev-parse --show-toplevel)" cmake -S . -B build/testing-native-linux -G Ninja \ -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ - -DBUILD_SHARED_LIBS=OFF \ + -DCHERYL_BUILD_SHARED_LIBS=OFF \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_SCAN_FOR_MODULES=OFF \ -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST \ - -DCHERYL_LOG_PROFILE=developer -DCHERYL_SANDBOX_BUILD=OFF \ + -DCHERYL_LOG_PROFILE=developer \ -DCHERYL_BUILD_NATIVE_GLFW=ON -DCHERYL_BUILD_OPENGL=ON \ -DCHERYL_NATIVE_INPUT=ON -DCHERYL_NATIVE_NULL_PLATFORM=OFF \ -DGAINPUT_ENABLE_HID=OFF -DGLFW_BUILD_X11=ON -DGLFW_BUILD_WAYLAND=OFF \ diff --git a/docs/development/ui-adapters.md b/docs/development/ui-adapters.md index 0ec4c42a..7fa8b49b 100644 --- a/docs/development/ui-adapters.md +++ b/docs/development/ui-adapters.md @@ -177,17 +177,16 @@ one owner is affected. A matching compiled build can be reused without rebuildin cd "$(git rev-parse --show-toplevel)" cmake -S . -B build/testing-native-ui -G Ninja \ -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ - -DBUILD_SHARED_LIBS=OFF \ + -DCHERYL_BUILD_SHARED_LIBS=OFF \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_SCAN_FOR_MODULES=OFF \ -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST \ - -DCHERYL_LOG_PROFILE=developer -DCHERYL_SANDBOX_BUILD=OFF \ + -DCHERYL_LOG_PROFILE=developer \ -DCHERYL_BUILD_NATIVE_GLFW=ON -DCHERYL_BUILD_OPENGL=ON \ -DCHERYL_NATIVE_INPUT=ON -DCHERYL_NATIVE_NULL_PLATFORM=OFF \ -DGAINPUT_ENABLE_HID=OFF -DGLFW_BUILD_X11=ON -DGLFW_BUILD_WAYLAND=OFF \ -DCHERYL_BUILD_UI_TGUI=ON -DCHERYL_BUILD_UI_RMLUI=ON \ -DCHERYL_TGUI_SOURCE="$PWD/extern/tgui" \ -DCHERYL_RMLUI_SOURCE="$PWD/extern/rmlui" \ - -DCHERYL_RMLUI_PLACEHOLDER_FIX_VERIFIED=OFF \ -DCHERYL_BUILD_AUDIO_MINIAUDIO=OFF -DCHERYL_BUILD_DEMO=ON \ -DCHERYL_BUILD_TESTS=ON -DCHERYL_BUILD_CONSUMER_TESTS=ON \ -DCHERYL_BUILD_ALL_TESTS=OFF -DCHERYL_BUILD_ACCEPTANCE_TESTS=OFF diff --git a/docs/external-work/rmlui-placeholder-issue.md b/docs/external-work/rmlui-placeholder-issue.md index 8449fa71..2816b1b0 100644 --- a/docs/external-work/rmlui-placeholder-issue.md +++ b/docs/external-work/rmlui-placeholder-issue.md @@ -225,7 +225,7 @@ The [demo setup and update](../../projects/apps/demo/src/rmlui-demo.cpp) load a Cheryl commit `f0a25b2203fcf809601ed8009c8964d8b3bedfef` adds the [source-build workaround](../../projects/modules/ui/rmlui/cmake/RmlUi.cmake). The [module dependency contract](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract) owns generated-source behavior and correction declarations. The guard changes C++ behavior; CMake arranges which source is compiled while leaving the dependency checkout unchanged. -The helper applies only when Cheryl creates the RmlUi source target. Existing `RmlUi::Core` targets and installed packages bypass it, but the [module](../../projects/modules/ui/rmlui/CMakeLists.txt) now rejects them unless they declare `CHERYL_RMLUI_PLACEHOLDER_FIX=TRUE` or the consumer attests through `CHERYL_RMLUI_PLACEHOLDER_FIX_VERIFIED=ON`. This declaration must describe a correction already present in the supplied library; it does not install or execute the guard. Neither dependency path has been runtime-validated for this report. Do not describe the local guard as a confirmed fix or use a guarded Cheryl build as the pre-fix upstream baseline. +The helper applies only when Cheryl creates the RmlUi source target. Existing `RmlUi::Core` targets and installed packages bypass it, but the [module](../../projects/modules/ui/rmlui/CMakeLists.txt) rejects them unless their concrete Core target declares `CHERYL_RMLUI_PLACEHOLDER_FIX=TRUE`. The [dependency contract](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract) shows how a provider or consumer can declare a verified correction, including when `RmlUi::Core` is an alias. This declaration must describe a correction already present in the supplied library; it does not install or execute the guard. Neither dependency path has been runtime-validated for this report. Do not describe the local guard as a confirmed fix or use a guarded Cheryl build as the pre-fix upstream baseline. ## Preparing a submission under RmlUi's AI policy diff --git a/docs/generation/Doxyfile.in b/docs/generation/Doxyfile.in index f9a9997a..a28586fb 100644 --- a/docs/generation/Doxyfile.in +++ b/docs/generation/Doxyfile.in @@ -41,8 +41,16 @@ STRIP_FROM_INC_PATH = @INCLUDE_ROOTS@ SHOW_INCLUDE_FILES = YES ENABLE_PREPROCESSING = YES -PREDEFINED = CHERYL_NATIVE_INPUT=1 -MACRO_EXPANSION = NO +# Visibility decorators do not belong to the documented C++ declarations. +# Expand only these definitions so other public macros retain their spelling. +PREDEFINED = CHERYL_NATIVE_INPUT=1 \ + CE_DETAIL_EXPORT= CE_DETAIL_IMPORT= CE_TYPE_API= CE_API= \ + CE_NATIVE_GLFW_API= CE_OPENGL_API= \ + CE_UI_TGUI_API= CE_UI_RMLUI_API= CE_AUDIO_MINIAUDIO_API= \ + CE_STARTUP_API= CE_OPENGL_STARTUP_API= CE_DEBUG_TERMINAL_API= \ + CE_WORKER_TOPOLOGY_HWLOC_API= CE_WORKER_TOPOLOGY_JSON_API= +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES SEARCH_INCLUDES = NO CLANG_ASSISTED_PARSING = NO diff --git a/docs/generation/conf.py b/docs/generation/conf.py index c7ef8c8f..c484e5e3 100644 --- a/docs/generation/conf.py +++ b/docs/generation/conf.py @@ -98,7 +98,8 @@ html_theme = "sphinx_rtd_theme" templates_path = ["templates"] html_static_path = ["static"] -html_css_files = ["hierarchy.css", "typography.css"] +html_css_files = ["hierarchy.css", "typography.css", "layout.css", "theme.css"] +html_js_files = ["appearance.js"] html_title = "Cheryl Engine documentation" html_theme_options = {"navigation_depth": 4, "collapse_navigation": False} html_show_sourcelink = True diff --git a/docs/generation/static/appearance.js b/docs/generation/static/appearance.js new file mode 100644 index 00000000..5957c14d --- /dev/null +++ b/docs/generation/static/appearance.js @@ -0,0 +1,49 @@ +/* Resolve the theme in the document head, before the page is painted. */ +(() => { + "use strict"; + + const storageKey = "cheryl-docs-theme"; + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)"); + let preference = "system"; + + try { + const saved = window.localStorage.getItem(storageKey); + if (saved === "light" || saved === "dark") { + preference = saved; + } + } catch { + // Theme switching still works when browser storage is unavailable. + } + + function applyTheme() { + document.documentElement.dataset.theme = preference === "system" + ? (systemTheme.matches ? "dark" : "light") + : preference; + } + + applyTheme(); + systemTheme.addEventListener("change", applyTheme); + + document.addEventListener("DOMContentLoaded", () => { + const select = document.getElementById("cheryl-theme"); + if (!select) { + return; + } + + select.value = preference; + select.closest(".cheryl-appearance").hidden = false; + select.addEventListener("change", () => { + preference = select.value; + applyTheme(); + try { + if (preference === "system") { + window.localStorage.removeItem(storageKey); + } else { + window.localStorage.setItem(storageKey, preference); + } + } catch { + // Keep the current page usable without persistent storage. + } + }); + }); +})(); diff --git a/docs/generation/static/layout.css b/docs/generation/static/layout.css new file mode 100644 index 00000000..ea4f0506 --- /dev/null +++ b/docs/generation/static/layout.css @@ -0,0 +1,18 @@ +@media screen { + /* Keep the theme's 800px content width when space permits, then grow with + the viewport. The parent already reserves space for the sidebar. */ + .wy-nav-content { + width: max(800px, 75vw); + max-width: 100%; + } + + /* The theme reserves at least 80% for values, leaving too little room for + field labels in nested API descriptions on narrow viewports. */ + html.writer-html5 .rst-content dl.field-list { + grid-template-columns: max-content minmax(0, 1fr); + } + + .rst-content dl.field-list > dd { + overflow-wrap: anywhere; + } +} diff --git a/docs/generation/static/theme.css b/docs/generation/static/theme.css new file mode 100644 index 00000000..a832c091 --- /dev/null +++ b/docs/generation/static/theme.css @@ -0,0 +1,409 @@ +/* Keep the appearance control available above the page on desktop and mobile. */ +.cheryl-appearance { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1rem; + font-size: 0.875rem; +} + +.cheryl-appearance[hidden] { + display: none; +} + +.cheryl-appearance label { + margin: 0; + color: inherit; + font-size: inherit; +} + +.cheryl-appearance select { + width: auto; + min-height: 2.25rem; + margin: 0; + padding: 0.25rem 0.5rem; + border: 1px solid #788594; + border-radius: 0.2rem; + background: #fff; + color: #253346; + font: inherit; +} + +.cheryl-appearance select:focus-visible { + outline: 2px solid #216aa0; + outline-offset: 3px; +} + +@media screen { + html[data-theme="light"] { + color-scheme: light; + } + + html[data-theme="dark"] { + color-scheme: dark; + --cheryl-background: #151b23; + --cheryl-surface: #1c2531; + --cheryl-surface-raised: #253246; + --cheryl-code-background: #101720; + --cheryl-text: #e1e8f0; + --cheryl-muted: #adbacb; + --cheryl-border: #526174; + --cheryl-link: #8ecaff; + --cheryl-link-hover: #c3e3ff; + --cheryl-visited: #d0b2ff; + --cheryl-inline-code: #ffca9b; + --cheryl-focus: #ffda82; + --cheryl-mark: #594a22; + scrollbar-color: var(--cheryl-border) var(--cheryl-background); + } + + html[data-theme="dark"] body, + html[data-theme="dark"] .wy-nav-content-wrap { + background: var(--cheryl-background); + color: var(--cheryl-text); + } + + html[data-theme="dark"] .wy-nav-content { + background: var(--cheryl-background); + } + + html[data-theme="dark"] a { + color: var(--cheryl-link); + } + + html[data-theme="dark"] a:visited { + color: var(--cheryl-visited); + } + + html[data-theme="dark"] a:hover { + color: var(--cheryl-link-hover); + } + + html[data-theme="dark"] :is(a, button, input, select, textarea):focus-visible { + outline: 2px solid var(--cheryl-focus); + outline-offset: 3px; + } + + html[data-theme="dark"] hr { + border-color: var(--cheryl-border); + } + + html[data-theme="dark"] footer, + html[data-theme="dark"] .cheryl-revision, + html[data-theme="dark"] #search-results .context, + html[data-theme="dark"] .rst-content :is(.footnote, .footnote-list, .citation, .citation-list) { + color: var(--cheryl-muted); + } + + html[data-theme="dark"] .wy-nav-side, + html[data-theme="dark"] .wy-side-nav-search, + html[data-theme="dark"] .wy-nav-top { + background: var(--cheryl-code-background); + color: var(--cheryl-text); + } + + html[data-theme="dark"] .wy-side-nav-search > a, + html[data-theme="dark"] .wy-nav-top a, + html[data-theme="dark"] .wy-menu-vertical a, + html[data-theme="dark"] .wy-menu-vertical a:visited { + color: var(--cheryl-text); + } + + html[data-theme="dark"] .wy-menu-vertical :is(header, p.caption) { + color: var(--cheryl-link); + } + + /* RTD assigns separate light surfaces at each expanded navigation level. */ + html[data-theme="dark"] .wy-menu-vertical li.current, + html[data-theme="dark"] .wy-menu-vertical li.current > a, + html[data-theme="dark"] .wy-menu-vertical li.current li > a, + html[data-theme="dark"] .wy-menu-vertical li.on > a { + border-color: var(--cheryl-border); + background: var(--cheryl-surface) !important; + color: var(--cheryl-text) !important; + } + + html[data-theme="dark"] .wy-menu-vertical a:hover, + html[data-theme="dark"] .wy-menu-vertical li.current a:hover, + html[data-theme="dark"] .wy-menu-vertical li.on a:hover { + background: var(--cheryl-surface-raised) !important; + color: var(--cheryl-link-hover) !important; + } + + html[data-theme="dark"] .wy-menu-vertical li button.toctree-expand { + color: var(--cheryl-muted) !important; + } + + html[data-theme="dark"] .wy-menu-vertical li button.toctree-expand:hover { + color: var(--cheryl-text) !important; + } + + html[data-theme="dark"] .wy-menu-vertical :is(code, tt) { + border: none; + background: inherit; + color: inherit; + } + + html[data-theme="dark"] .rst-versions, + html[data-theme="dark"] .rst-versions .rst-current-version { + background: var(--cheryl-code-background); + color: var(--cheryl-text); + } + + html[data-theme="dark"] .rst-versions .rst-other-versions { + color: var(--cheryl-muted); + } + + html[data-theme="dark"] :is(input[type="text"], input[type="search"], textarea, select), + html[data-theme="dark"] .cheryl-appearance select { + border-color: var(--cheryl-border); + background: var(--cheryl-surface); + color: var(--cheryl-text); + } + + html[data-theme="dark"] input::placeholder, + html[data-theme="dark"] textarea::placeholder { + color: var(--cheryl-muted); + opacity: 1; + } + + html[data-theme="dark"] .wy-side-nav-search input[type="text"] { + border-color: var(--cheryl-border); + } + + html[data-theme="dark"] .btn-neutral, + html[data-theme="dark"] .btn-neutral:visited { + border-color: var(--cheryl-border); + background: var(--cheryl-surface) !important; + color: var(--cheryl-text) !important; + } + + html[data-theme="dark"] .btn-neutral:hover { + background: var(--cheryl-surface-raised) !important; + color: var(--cheryl-link-hover) !important; + } + + html[data-theme="dark"] .rst-content :is(table.docutils, table.field-list, .wy-table), + html[data-theme="dark"] .rst-content :is(table.docutils, table.field-list, .wy-table) :is(th, td) { + border-color: var(--cheryl-border); + } + + html[data-theme="dark"] .rst-content :is(table.docutils, table.field-list, .wy-table) :is(caption, thead) { + color: var(--cheryl-text); + } + + html[data-theme="dark"] .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td, + html[data-theme="dark"] .wy-table-backed, + html[data-theme="dark"] .wy-table-odd td, + html[data-theme="dark"] .wy-table-striped tr:nth-child(2n-1) td { + background: var(--cheryl-surface); + } + + html[data-theme="dark"] .rst-content :is(code, tt), + html[data-theme="dark"] code { + border-color: var(--cheryl-border); + background: var(--cheryl-surface); + color: var(--cheryl-text); + } + + html[data-theme="dark"] .rst-content :is(code.literal, tt.literal) { + color: var(--cheryl-inline-code); + } + + html[data-theme="dark"] .rst-content a :is(code, tt) { + color: var(--cheryl-link); + } + + html[data-theme="dark"] .rst-content :is(.guilabel, .menuselection, kbd, .kbd) { + border-color: var(--cheryl-border) !important; + background: var(--cheryl-surface) !important; + color: var(--cheryl-text) !important; + box-shadow: none !important; + } + + /* The upstream signature selectors have unusually high specificity. */ + html[data-theme="dark"] .rst-content dl.cpp > dt { + border-color: var(--cheryl-link) !important; + background: var(--cheryl-surface-raised) !important; + color: var(--cheryl-text) !important; + } + + html[data-theme="dark"] .rst-content dl.cpp dl.cpp > dt { + border-color: var(--cheryl-border) !important; + background: var(--cheryl-surface) !important; + } + + html[data-theme="dark"] .rst-content dl.cpp :is(.sig-name, .descname, .descclassname, .optional) { + color: var(--cheryl-text) !important; + } + + html[data-theme="dark"] .rst-content dl.cpp dt :is(.headerlink, a), + html[data-theme="dark"] .rst-content .toc-backref { + color: var(--cheryl-link) !important; + } + + html[data-theme="dark"] .rst-content dl.cpp > dt::before { + color: var(--cheryl-link) !important; + } + + html[data-theme="dark"] .rst-content :is(.viewcode-back, .viewcode-link) { + color: #94d9ad; + } + + html[data-theme="dark"] .rst-content .sidebar { + border-color: var(--cheryl-border); + background: var(--cheryl-surface); + } + + html[data-theme="dark"] .rst-content .sidebar .sidebar-title { + background: var(--cheryl-surface-raised); + } + + html[data-theme="dark"] .rst-content :is(.admonition, .admonition-todo, .attention, .caution, .danger, .error, .hint, .important, .note, .seealso, .tip, .warning), + html[data-theme="dark"] .wy-alert { + background: #1d3142; + color: var(--cheryl-text); + } + + html[data-theme="dark"] .rst-content .admonition-title, + html[data-theme="dark"] .wy-alert-title { + background: #31516d; + color: #f2f6fb; + } + + html[data-theme="dark"] .rst-content :is(.note, .seealso, .wy-alert-info) { + background: #1d3142; + } + + html[data-theme="dark"] .rst-content :is(.note, .seealso, .wy-alert-info) .admonition-title { + background: #31516d; + } + + html[data-theme="dark"] .rst-content :is(.admonition-todo, .attention, .caution, .warning, .wy-alert-warning) { + background: #3b2e1e; + } + + html[data-theme="dark"] .rst-content :is(.admonition-todo, .attention, .caution, .warning, .wy-alert-warning) .admonition-title { + background: #69502d; + } + + html[data-theme="dark"] .rst-content :is(.danger, .error, .wy-alert-danger) { + background: #40272c; + } + + html[data-theme="dark"] .rst-content :is(.danger, .error, .wy-alert-danger) .admonition-title { + background: #733d46; + } + + html[data-theme="dark"] .rst-content :is(.hint, .important, .tip, .wy-alert-success) { + background: #1d372e; + } + + html[data-theme="dark"] .rst-content :is(.hint, .important, .tip, .wy-alert-success) .admonition-title { + background: #315c4b; + } + + html[data-theme="dark"] .rst-content .wy-alert-neutral { + background: var(--cheryl-surface); + } + + html[data-theme="dark"] .rst-content .wy-alert-neutral .admonition-title { + background: var(--cheryl-surface-raised); + color: var(--cheryl-text); + } + + html[data-theme="dark"] .rst-content .admonition :is(th, td) { + border-color: var(--cheryl-border) !important; + } + + html[data-theme="dark"] #file-hierarchy ul ul > li::before, + html[data-theme="dark"] #file-hierarchy ul ul > li::after { + border-color: var(--cheryl-border); + } + + html[data-theme="dark"] :is(mark, .highlighted), + html[data-theme="dark"] .rst-content .highlighted { + background: var(--cheryl-mark); + color: var(--cheryl-text); + box-shadow: 0 0 0 2px var(--cheryl-mark); + } + + html[data-theme="dark"] .rst-content :is(dt:target, .footnote:target), + html[data-theme="dark"] .viewcode-block:target { + background-color: var(--cheryl-mark) !important; + } + + html[data-theme="dark"] .rst-content .linenodiv pre, + html[data-theme="dark"] .rst-content div[class^="highlight"], + html[data-theme="dark"] .rst-content div[class^="highlight"] pre, + html[data-theme="dark"] .rst-content pre.literal-block, + html[data-theme="dark"] .highlight { + border-color: var(--cheryl-border); + background: var(--cheryl-code-background); + color: var(--cheryl-text); + } + + html[data-theme="dark"] .rst-content .code-block-caption { + color: var(--cheryl-muted); + } + + html[data-theme="dark"] .highlight .hll, + html[data-theme="dark"] :is(td.linenos, span.linenos) .special, + html[data-theme="dark"] span.linenos.special { + background: var(--cheryl-mark); + color: var(--cheryl-text); + } + + html[data-theme="dark"] :is(td.linenos pre, span.linenos) { + color: var(--cheryl-muted); + } + + html[data-theme="dark"] .highlight :is(.c, .ch, .cm, .cpf, .c1, .cs, .go, .w) { + color: #a8b8cb; + } + + html[data-theme="dark"] .highlight :is(.cp, .k, .kc, .kd, .kn, .kp, .kr, .kt, .nt, .ow) { + color: #c3a6ff; + } + + html[data-theme="dark"] .highlight :is(.m, .mb, .mf, .mh, .mi, .mo, .il, .no) { + color: #ffca91; + } + + html[data-theme="dark"] .highlight :is(.s, .sa, .sb, .sc, .dl, .sd, .s1, .s2, .sh, .sx, .ss) { + color: #a9dca1; + } + + html[data-theme="dark"] .highlight :is(.se, .si, .sr, .na, .nd, .nl) { + color: #f3d889; + } + + html[data-theme="dark"] .highlight :is(.nb, .bp, .nc, .nf, .fm, .nn, .gh, .gp, .gu, .gt) { + color: #8ecaff; + } + + html[data-theme="dark"] .highlight :is(.ni, .nv, .vc, .vg, .vi, .vm, .o) { + color: var(--cheryl-text); + } + + html[data-theme="dark"] .highlight :is(.gd, .gr, .ne) { + color: #ffaaaa; + } + + html[data-theme="dark"] .highlight .gi { + color: #a9dca1; + } + + html[data-theme="dark"] .highlight .err { + border-color: #ffaaaa; + color: #ffaaaa; + } +} + +@media print { + .cheryl-appearance { + display: none; + } +} diff --git a/docs/generation/templates/layout.html b/docs/generation/templates/layout.html index 9585e0c9..5b9382c1 100644 --- a/docs/generation/templates/layout.html +++ b/docs/generation/templates/layout.html @@ -1,5 +1,13 @@ {% extends "!layout.html" %} {% block content %} + {{ super() }}

Source revision: diff --git a/docs/generation/tests/test_configuration.py b/docs/generation/tests/test_configuration.py index 55f70fcc..fdd72acd 100644 --- a/docs/generation/tests/test_configuration.py +++ b/docs/generation/tests/test_configuration.py @@ -3,11 +3,51 @@ import ast from fnmatch import fnmatchcase from pathlib import Path +import re +import shlex import tempfile import unittest CONFIGURATION = Path(__file__).resolve().parents[1] / "conf.py" +EXPORT_HEADER = CONFIGURATION.parents[2] / "projects/engine/include/cheryl/export.h" + + +class DoxygenTests(unittest.TestCase): + def setUp(self): + source = CONFIGURATION.with_name("Doxyfile.in").read_text(encoding="utf-8") + self.settings = {} + for line in re.sub(r"\\\r?\n", " ", source).splitlines(): + assignment = re.match(r"\s*([A-Z_]+)\s*(\+?=)\s*(.*)", line) + if assignment: + name, operator, value = assignment.groups() + tokens = shlex.split(value, comments=True) + self.settings[name] = self.settings.get(name, []) + tokens if operator == "+=" else tokens + + def test_selective_expansion(self): + for name in ("ENABLE_PREPROCESSING", "MACRO_EXPANSION", "EXPAND_ONLY_PREDEF"): + with self.subTest(setting=name): + self.assertEqual(self.settings.get(name), ["YES"]) + self.assertEqual(self.settings.get("SEARCH_INCLUDES"), ["NO"]) + self.assertFalse(self.settings.get("EXPAND_AS_DEFINED")) + + def test_export_decorators(self): + # Read the owning header so a newly added component cannot silently + # introduce another decorator into Breathe/Sphinx declarations. + macros = set(re.findall( + r"^\s*#\s*define\s+(CE_[A-Z0-9_]+)\b", + EXPORT_HEADER.read_text(encoding="utf-8"), re.MULTILINE, + )) + self.assertIn("CE_API", macros) + self.assertIn("CE_TYPE_API", macros) + definitions = {} + for token in self.settings.get("PREDEFINED", []): + name, equals, value = token.partition("=") + definitions[name.removesuffix(":")] = value if equals else "1" + self.assertEqual(definitions.get("CHERYL_NATIVE_INPUT"), "1") + for name in sorted(macros): + with self.subTest(decorator=name): + self.assertEqual(definitions.get(name), "", "Export decorators must expand to nothing") class ExclusionTests(unittest.TestCase): diff --git a/docs/getting-started/project-setup.md b/docs/getting-started/project-setup.md index 27f70d67..468e0705 100644 --- a/docs/getting-started/project-setup.md +++ b/docs/getting-started/project-setup.md @@ -157,6 +157,9 @@ for other platform choices. No separate engine build or SDK installation is required. To run the maintained example before writing your own entry point, follow [First game: setup](first-game.md#setup). +On Windows, complete the [shared-library runtime setup](../development/consuming-engine.md#windows-source-build-runtime) +before launching the source-built game. + ### Choose smaller target sets when needed Link `Cheryl::Cheryl` for all selected libraries, or choose individual public @@ -282,12 +285,14 @@ separate build directories and installed SDK prefixes for different configuratio and module selections. Compile each game `.cpp` file against the headers and target settings that belong to those libraries. For a packaged SDK, check its `SDK-MANIFEST.json` for the producer's source/submodule revisions, features, -toolchain and configuration. Matching filenames alone do not establish compatibility. - -`Cheryl::Engine`, `Cheryl::Startup` and the current compiled module implementations -build as static libraries, whose code is linked into the executable. Setting -`BUILD_SHARED_LIBS=ON` does not convert these targets to shared libraries loaded -at runtime. Some third-party/native dependencies have their own linkage choices. -A shared-library distribution and its binary compatibility policy require a -separate design decision; use the current source-build or matched-SDK approach -for this walkthrough. +toolchain, configuration, linkage and enabled component versions. Matching filenames +alone do not establish compatibility. + +Cheryl defaults to shared Engine, modules and support libraries. Set +`CHERYL_BUILD_SHARED_LIBS=OFF` when configuring a source build or producing an +SDK to select static libraries, whose code is linked into the executable. The same +public targets and includes work with either selection; keep their build directories and SDK +prefixes separate. A shared game also needs its Cheryl and dependency libraries +available at runtime. Follow the [SDK runtime-path contract](../development/consuming-engine.md#prebuilt-development-sdks) +and [linkage reference](../development/building.md#library-linkage) for deployment +and versioned artifact names. diff --git a/docs/planning/long-term/platform-acceptance.md b/docs/planning/long-term/platform-acceptance.md index 302bb0be..cfef7f74 100644 --- a/docs/planning/long-term/platform-acceptance.md +++ b/docs/planning/long-term/platform-acceptance.md @@ -50,7 +50,7 @@ try { -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 ` -DCMAKE_CXX_SCAN_FOR_MODULES=OFF ` -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST ` - -DCHERYL_LOG_PROFILE=developer -DCHERYL_SANDBOX_BUILD=OFF ` + -DCHERYL_LOG_PROFILE=developer ` -DCHERYL_BUILD_NATIVE_GLFW=ON -DCHERYL_BUILD_OPENGL=ON ` -DCHERYL_NATIVE_INPUT=ON -DCHERYL_NATIVE_NULL_PLATFORM=OFF ` -DGAINPUT_ENABLE_HID=ON ` @@ -204,7 +204,8 @@ until those configurations are implemented and accepted. Full Wayland and other operating systems need their own runtime acceptance for input, rendering, UI coexistence, resizing and shutdown in sequential/concurrent -modes and normal/sandbox configurations. Establish machines, fixtures and native +modes and their selected `CHERYL_NATIVE_INPUT` / `CHERYL_NATIVE_NULL_PLATFORM` +configurations. Establish machines, fixtures and native observation requirements when activating each platform; do not infer acceptance from Linux/X11 or software-driver execution. Reuse the [architecture procedures](../../development/architecture-validation.md) and the diff --git a/docs/runtime/worker-execution.md b/docs/runtime/worker-execution.md index fe6cdca6..a1d5b4b6 100644 --- a/docs/runtime/worker-execution.md +++ b/docs/runtime/worker-execution.md @@ -42,8 +42,8 @@ Neutral topology descriptions, pool/group options and scheduling live in `CE::Workers::Hwloc::make_topology_provider()`; `Cheryl::WorkerTopology::Json` supplies configuration loading independently of hwloc. Both live under `projects/engine/support/worker-topology/`. Applications link the selected support targets; Engine does not link back to them. -Public contracts expose neither hwloc nor JSON library types. The static hwloc -support library propagates its required final link dependency. +Public contracts expose neither hwloc nor JSON library types. For static builds, +the hwloc support target propagates its required final link dependency. Supply a `CE::Workers::TopologyProvider` through `CE::Workers::PoolOptions::topology_provider`. The pool retains it through worker joins. There is no global registrar, entry-point diff --git a/projects/apps/demo/README.md b/projects/apps/demo/README.md index b44181c0..257083aa 100644 --- a/projects/apps/demo/README.md +++ b/projects/apps/demo/README.md @@ -3,7 +3,7 @@ The demo composes `Cheryl::Engine`, `Cheryl::NativeGLFW`, `Cheryl::OpenGL` and `Cheryl::OpenGL::Startup` for shared command-line bootstrap. Root composition also selects the optional [Linux Debug terminal](../../modules/platform/debug-terminal-linux/README.md) -where available; actual Debug defaults to a separate output viewer. +in supported single-configuration Debug builds by default, with a separate output viewer. Selecting `Cheryl::UI::TGUI` adds a panel on the right; selecting `Cheryl::UI::RmlUi` adds an independent native-document view on the left. Each has live counters, a camera-reset button, an editable field, a scrolling list, @@ -19,7 +19,8 @@ initialized, configure the demo: ( set -e cd "$(git rev-parse --show-toplevel)" - cmake -S . -B build/release -DCMAKE_BUILD_TYPE=Release -G Ninja + cmake -S . -B build/release -DCMAKE_BUILD_TYPE=Release -G Ninja \ + -DCHERYL_BUILD_DEMO=ON ) ``` @@ -44,9 +45,9 @@ Run in sequential or concurrent mode: ) ``` -The demo requires Native GLFW, OpenGL and native input. TGUI is selected by -default; enable `CHERYL_BUILD_UI_RMLUI=ON` for the left-hand view. Disable both -adapters for the toolkit-free F2 text probe. Reload CMake after changing cached +The demo requires Native GLFW, OpenGL and native input. TGUI and RmlUi are +selected by default, showing both views. Disable both adapters for the toolkit-free +F2 text probe. Reload CMake after changing cached selections; the [build guide](../../../docs/development/building.md) owns option defaults and dependency settings. @@ -139,11 +140,13 @@ that stripped TRACE before starting the runtime. ## Native output terminal -On Linux GNU/Clang, root `AUTO` composition attaches the output terminal in actual -Debug builds. `--no-debug-terminal` disables display when included. Release and -other configurations omit it unless `CHERYL_DEBUG_TERMINAL=ON`; those included -non-Debug builds display only with `--debug-terminal`. Without an implementation -these options are not consumed by terminal support. Help/headless launches create +On Linux GNU/Clang, a fresh root Debug configuration using Ninja defaults +`CHERYL_BUILD_DEBUG_TERMINAL_LINUX` to `ON`. Other configurations default to `OFF`; +set it explicitly to include the terminal in Release or a multi-configuration build. +Existing build directories retain their cached selection. `--no-debug-terminal` +disables display when included. Included non-Debug builds display only with +`--debug-terminal`. Without an implementation these options are not consumed by +terminal support. Help/headless launches create no viewer. Closing the viewer keeps the demo running; normal exit closes it and abnormal process loss retains available output. See the [module contract](../../modules/platform/debug-terminal-linux/README.md) and diff --git a/projects/apps/first-game/CMakeLists.txt b/projects/apps/first-game/CMakeLists.txt index 79eba115..2d9583f4 100644 --- a/projects/apps/first-game/CMakeLists.txt +++ b/projects/apps/first-game/CMakeLists.txt @@ -4,7 +4,11 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) project(cheryl_first_game LANGUAGES CXX) endif() -set(CHERYL_FIRST_GAME_ENGINE_SOURCE "" CACHE PATH "Cheryl checkout for source consumption; empty uses an installed SDK") +# A copied example can obtain these definitions from its installed SDK instead. +include("${CMAKE_CURRENT_LIST_DIR}/../../../cmake/CherylOptions.cmake" OPTIONAL) +if(COMMAND cheryl_configure_first_game_options) + cheryl_configure_first_game_options() +endif() set(first_game_source_consumer OFF) if(NOT TARGET Cheryl::OpenGL::Startup) if(CHERYL_FIRST_GAME_ENGINE_SOURCE) @@ -41,6 +45,13 @@ if(NOT TARGET Cheryl::OpenGL::Startup) endif() endif() +if(NOT COMMAND cheryl_configure_first_game_options AND Cheryl_DIR) + include("${Cheryl_DIR}/CherylOptions.cmake" OPTIONAL) +endif() +if(COMMAND cheryl_configure_first_game_options) + cheryl_configure_first_game_options() +endif() + foreach(first_game_dependency IN ITEMS Cheryl::Engine Cheryl::NativeGLFW Cheryl::OpenGL Cheryl::OpenGL::Startup) if(NOT TARGET ${first_game_dependency}) message(FATAL_ERROR "first-game requires ${first_game_dependency}; select an SDK with Native GLFW, OpenGL and startup") diff --git a/projects/engine/CMakeLists.txt b/projects/engine/CMakeLists.txt index 3cbea58a..9fc2464e 100644 --- a/projects/engine/CMakeLists.txt +++ b/projects/engine/CMakeLists.txt @@ -6,7 +6,7 @@ file(GLOB_RECURSE CHERYL_ENGINE_SOURCES CONFIGURE_DEPENDS add_subdirectory(support/logging-config) add_subdirectory(support/signal-handlers) -add_library(${TARGET_LIB_ENGINE} STATIC ${CHERYL_ENGINE_SOURCES}) +add_library(${TARGET_LIB_ENGINE} ${CHERYL_LIBRARY_TYPE} ${CHERYL_ENGINE_SOURCES}) add_library(Cheryl::Engine ALIAS ${TARGET_LIB_ENGINE}) cheryl_register_public_component(${TARGET_LIB_ENGINE}) set_target_properties(${TARGET_LIB_ENGINE} PROPERTIES @@ -14,6 +14,7 @@ set_target_properties(${TARGET_LIB_ENGINE} PROPERTIES OUTPUT_NAME "${OUTPUT_LIB_ENGINE}" ARCHIVE_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}" CHERYL_REPOSITORY_ROOT "${CHERYL_REPOSITORY_ROOT}") +cheryl_configure_library(${TARGET_LIB_ENGINE} ENGINE) target_compile_features(${TARGET_LIB_ENGINE} PUBLIC cxx_std_23) target_include_directories(${TARGET_LIB_ENGINE} PUBLIC "$" @@ -27,9 +28,13 @@ target_link_libraries(${TARGET_LIB_ENGINE} PUBLIC ${LINKAGE_PUBLIC_LIB_ENGINE} PRIVATE ${LINKAGE_PRIVATE_LIB_ENGINE}) include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/FontFallback.cmake") -# Forward the bootstrap object to final consumers; an unreferenced archive member -# would not reliably install the original Debug crash handlers. -target_link_libraries(${TARGET_LIB_ENGINE} INTERFACE ${LINKAGE_INTERFACE_LIB_ENGINE}) +# A shared Engine owns one bootstrap. Static archives forward it so an +# unreferenced archive member cannot discard the original Debug crash handlers. +if(CHERYL_BUILD_SHARED_LIBS) + target_link_libraries(${TARGET_LIB_ENGINE} PRIVATE ${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS}) +else() + target_link_libraries(${TARGET_LIB_ENGINE} INTERFACE ${LINKAGE_INTERFACE_LIB_ENGINE}) +endif() add_dependencies(${TARGET_LIB_ENGINE} ${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS}) if(NOT MSVC AND CMAKE_BUILD_TYPE STREQUAL "Debug") # Root Debug compilation is instrumented; standalone consumers also need diff --git a/projects/engine/include/cheryl/assets/definitions/animation.h b/projects/engine/include/cheryl/assets/definitions/animation.h index ebcc293a..10f2264c 100644 --- a/projects/engine/include/cheryl/assets/definitions/animation.h +++ b/projects/engine/include/cheryl/assets/definitions/animation.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -39,7 +41,7 @@ namespace CE::Assets::Tilesets { // Exact duration boundaries enter the next frame; loops wrap and other clips // hold their last cell. Negative time, empty/nonpositive or overflowing // timelines throw invalid_args. Cell/grid bounds remain the caller's contract. - [[nodiscard]] CE::Assets::Grids::CellIndex cell_at(std::chrono::milliseconds elapsed) const; + [[nodiscard]] CE_API CE::Assets::Grids::CellIndex cell_at(std::chrono::milliseconds elapsed) const; }; } diff --git a/projects/engine/include/cheryl/assets/definitions/grid.h b/projects/engine/include/cheryl/assets/definitions/grid.h index f5800fb3..57c9ae67 100644 --- a/projects/engine/include/cheryl/assets/definitions/grid.h +++ b/projects/engine/include/cheryl/assets/definitions/grid.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -22,11 +24,11 @@ namespace CE::Assets::Grids { std::size_t columns{}; // Count rejects size_t multiplication overflow; addressing rejects out-of-range cells. - [[nodiscard]] std::size_t cell_count() const; - [[nodiscard]] CellIndex cell_index(std::size_t row, std::size_t column) const; - [[nodiscard]] CE::Rendering::Geometry::PixelRect cell_rect(CellIndex cell) const; - [[nodiscard]] std::uint64_t occupied_right() const; - [[nodiscard]] std::uint64_t occupied_bottom() const; + [[nodiscard]] CE_API std::size_t cell_count() const; + [[nodiscard]] CE_API CellIndex cell_index(std::size_t row, std::size_t column) const; + [[nodiscard]] CE_API CE::Rendering::Geometry::PixelRect cell_rect(CellIndex cell) const; + [[nodiscard]] CE_API std::uint64_t occupied_right() const; + [[nodiscard]] CE_API std::uint64_t occupied_bottom() const; }; struct View { diff --git a/projects/engine/include/cheryl/assets/definitions/manifest.h b/projects/engine/include/cheryl/assets/definitions/manifest.h index ce7c5b26..75675c95 100644 --- a/projects/engine/include/cheryl/assets/definitions/manifest.h +++ b/projects/engine/include/cheryl/assets/definitions/manifest.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -30,6 +32,6 @@ namespace CE::Assets::Manifests { // Owned deduplicated entry paths, sprites then tilesets in first-reference // order. The manifest-level default is included only when an entry uses it. - [[nodiscard]] std::vector textures() const; + [[nodiscard]] CE_API std::vector textures() const; }; } diff --git a/projects/engine/include/cheryl/assets/definitions/pipeline.h b/projects/engine/include/cheryl/assets/definitions/pipeline.h index d6137ade..39b0a213 100644 --- a/projects/engine/include/cheryl/assets/definitions/pipeline.h +++ b/projects/engine/include/cheryl/assets/definitions/pipeline.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -60,5 +62,5 @@ namespace CE::Rendering::Materials { namespace CE::Rendering::Pipelines { // CPU-only structural validation; throws on empty paths, unsupported layout/ // topology/state, disabled-depth writes or an invalid parameter contract. - void validate(const Definition& definition); + CE_API void validate(const Definition& definition); } diff --git a/projects/engine/include/cheryl/assets/definitions/shader-assets.h b/projects/engine/include/cheryl/assets/definitions/shader-assets.h index 0f3bd64e..dea1cdda 100644 --- a/projects/engine/include/cheryl/assets/definitions/shader-assets.h +++ b/projects/engine/include/cheryl/assets/definitions/shader-assets.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -54,10 +56,11 @@ namespace CE::Assets::Shaders { std::vector materials; }; - [[nodiscard]] bool is_qualified_id(std::string_view id); - [[nodiscard]] CE::Rendering::Parameters::Value parameter_value(const Literal& literal); - [[nodiscard]] CE::Rendering::Parameters::Set defaults(const MaterialRecipe& recipe); - [[nodiscard]] CE::Rendering::Pipelines::Definition pipeline_definition(const MaterialRecipe& recipe, const ProgramRecipe& program); - void validate_program(const ProgramRecipe& recipe); - void validate_material(const MaterialRecipe& recipe, const ProgramRecipe& program); + [[nodiscard]] CE_API bool is_qualified_id(std::string_view id); + [[nodiscard]] CE_API CE::Rendering::Parameters::Value parameter_value(const Literal& literal); + [[nodiscard]] CE_API CE::Rendering::Parameters::Set defaults(const MaterialRecipe& recipe); + [[nodiscard]] CE_API CE::Rendering::Pipelines::Definition + pipeline_definition(const MaterialRecipe& recipe, const ProgramRecipe& program); + CE_API void validate_program(const ProgramRecipe& recipe); + CE_API void validate_material(const MaterialRecipe& recipe, const ProgramRecipe& program); } diff --git a/projects/engine/include/cheryl/assets/definitions/sprite.h b/projects/engine/include/cheryl/assets/definitions/sprite.h index d1a3615d..d6c13ca3 100644 --- a/projects/engine/include/cheryl/assets/definitions/sprite.h +++ b/projects/engine/include/cheryl/assets/definitions/sprite.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -23,6 +25,6 @@ namespace CE::Assets::Sprites { std::vector animations; std::optional shader = {}; - [[nodiscard]] std::string id() const; + [[nodiscard]] CE_API std::string id() const; }; } diff --git a/projects/engine/include/cheryl/assets/definitions/tileset.h b/projects/engine/include/cheryl/assets/definitions/tileset.h index 8ceafda6..d9f99a8d 100644 --- a/projects/engine/include/cheryl/assets/definitions/tileset.h +++ b/projects/engine/include/cheryl/assets/definitions/tileset.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -29,7 +31,7 @@ namespace CE::Assets::Tilesets { using WangSignature = std::array; struct WangSignatureHash { - [[nodiscard]] std::size_t operator()(const WangSignature& signature) const noexcept; + [[nodiscard]] CE_API std::size_t operator()(const WangSignature& signature) const noexcept; }; struct WangTile { @@ -74,6 +76,6 @@ namespace CE::Assets::Tilesets { std::unordered_map autotiles; std::optional shader = {}; - [[nodiscard]] std::string id() const; + [[nodiscard]] CE_API std::string id() const; }; } diff --git a/projects/engine/include/cheryl/assets/geometry/grid-geometry.h b/projects/engine/include/cheryl/assets/geometry/grid-geometry.h index 3b153ca1..cd484ab5 100644 --- a/projects/engine/include/cheryl/assets/geometry/grid-geometry.h +++ b/projects/engine/include/cheryl/assets/geometry/grid-geometry.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -18,6 +20,6 @@ namespace CE::Assets::Grids { // Local Y-up pixels around the normalized top-left pivot, normalized UVs. Throws // for empty texture dimensions, out-of-image bounds or an excessive vertex count. // Direct grid/pivot values must satisfy the manifest's representability constraints. - [[nodiscard]] GeometryData + [[nodiscard]] CE_API GeometryData make_geometry(const Definition& grid, CE::Rendering::Geometry::Pivot pivot, CE::Rendering::Geometry::PixelSize texture_size); } diff --git a/projects/engine/include/cheryl/assets/resources/decoded-image.h b/projects/engine/include/cheryl/assets/resources/decoded-image.h index 3330fb37..c47d4b3d 100644 --- a/projects/engine/include/cheryl/assets/resources/decoded-image.h +++ b/projects/engine/include/cheryl/assets/resources/decoded-image.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "image.h" #include @@ -13,5 +15,5 @@ namespace CE::Assets::Images { }; // CPU-only file decode; throws on unreadable/unsupported data or storage overflow. // Returns owned pixels without publishing a resource/cache entry. - [[nodiscard]] Decoded decode(const std::filesystem::path& file); + [[nodiscard]] CE_API Decoded decode(const std::filesystem::path& file); } diff --git a/projects/engine/include/cheryl/assets/resources/geometry2d.h b/projects/engine/include/cheryl/assets/resources/geometry2d.h index 72ceecfe..fdda536b 100644 --- a/projects/engine/include/cheryl/assets/resources/geometry2d.h +++ b/projects/engine/include/cheryl/assets/resources/geometry2d.h @@ -1,5 +1,7 @@ #pragma once +#include + #include namespace CE::Rendering::Geometry { @@ -8,7 +10,7 @@ namespace CE::Rendering::Geometry { // Retained immutable upload, independent of material/image bindings. Logical // ownership can outlive shutdown; native operations require its live backend domain. - struct Buffer2D { + struct CE_API Buffer2D { virtual ~Buffer2D() = default; // Immutable upload metadata; safe to inspect during CPU frame preparation. [[nodiscard]] virtual Layout2D vertex_layout() const noexcept = 0; diff --git a/projects/engine/include/cheryl/assets/resources/image.h b/projects/engine/include/cheryl/assets/resources/image.h index a106d9cd..8de99316 100644 --- a/projects/engine/include/cheryl/assets/resources/image.h +++ b/projects/engine/include/cheryl/assets/resources/image.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -8,7 +10,7 @@ namespace CE::Rendering::Resources { // Immutable uploaded contents/dimensions in the selected backend domain. // Retain this handle in frames/materials; changed pixels require a new image. // Logical ownership after backend shutdown does not permit native bind/use. - struct Image { + struct CE_API Image { virtual ~Image() = default; // Immutable pixel dimensions; CPU inspection does not bind the resource. [[nodiscard]] virtual CE::Rendering::Geometry::PixelSize pixel_size() const = 0; diff --git a/projects/engine/include/cheryl/assets/resources/parameters.h b/projects/engine/include/cheryl/assets/resources/parameters.h index 8b8e2513..a36cfdde 100644 --- a/projects/engine/include/cheryl/assets/resources/parameters.h +++ b/projects/engine/include/cheryl/assets/resources/parameters.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "image.h" #include "sampler.h" #include "shader.h" @@ -44,22 +46,22 @@ namespace CE::Rendering::Parameters { using Contract = std::vector; - [[nodiscard]] Type type_of(const Value& value); + [[nodiscard]] CE_API Type type_of(const Value& value); // Throws invalid_args for malformed keys/types/semantics/defaults. Does not // reflect a program or validate image domains, unit limits or numeric ranges. - void validate_contract(const Contract& contract); + CE_API void validate_contract(const Contract& contract); // Validate one partial custom layer: rejects unknown/semantic keys, wrong types // and null images; does not require complete values or distinct sampler units. - void validate_values(const Contract& contract, const Set& values); + CE_API void validate_values(const Contract& contract, const Set& values); // Validate a complete packet, including already resolved engine values. Requires // all required keys and distinct sampler units; does not verify semantic provenance. - void validate_resolved(const Contract& contract, const Set& values); + CE_API void validate_resolved(const Contract& contract, const Set& values); // Copies defaults < pass < material < draw. Engine semantics have a single owner // and cannot be overridden by a custom-value layer. Validate every input layer, // even overridden entries, then the complete result. Optional missing keys are // absent. Failure leaves inputs unchanged and returns no partially resolved set. - [[nodiscard]] Set resolve( + [[nodiscard]] CE_API Set resolve( const Contract& contract, const PassSemantics& pass_semantics, const DrawSemantics& draw_semantics, diff --git a/projects/engine/include/cheryl/assets/resources/pipeline.h b/projects/engine/include/cheryl/assets/resources/pipeline.h index d7cefa23..f571df35 100644 --- a/projects/engine/include/cheryl/assets/resources/pipeline.h +++ b/projects/engine/include/cheryl/assets/resources/pipeline.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -7,21 +9,21 @@ namespace CE::Rendering::Resources { // A backend subclass retains its executable resource. Construction takes a // definition snapshot; publishing another instance creates another generation. - class Pipeline { + class CE_TYPE_API Pipeline { const CE::Rendering::Pipelines::Definition definition_; protected: - explicit Pipeline(CE::Rendering::Pipelines::Definition definition); + CE_API explicit Pipeline(CE::Rendering::Pipelines::Definition definition); public: - virtual ~Pipeline() = default; + CE_API virtual ~Pipeline(); Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; // Borrowed immutable metadata, valid while this Pipeline is retained. [[nodiscard]] const CE::Rendering::Pipelines::Definition& definition() const { return definition_; } // CPU metadata check: matching layout/topology, nonempty bounded vertex range, // complete primitives and pass constraints. No native bind/domain check. - void validate_draw( + CE_API void validate_draw( const CE::Rendering::Geometry::Buffer2D& geometry, std::size_t first_vertex, std::size_t vertex_count, @@ -37,11 +39,11 @@ namespace CE::Rendering::Resources { public: // A null pipeline or malformed defaults throw before a Material is returned. - explicit Material(CE::Rendering::Materials::Definition definition); + CE_API explicit Material(CE::Rendering::Materials::Definition definition); // Borrowed metadata, valid while this Material is retained. [[nodiscard]] const CE::Rendering::Materials::Definition& definition() const { return definition_; } // Owned resolved copy; validation failure leaves the recipe and caller inputs intact. - [[nodiscard]] CE::Rendering::Parameters::Set resolve( + [[nodiscard]] CE_API CE::Rendering::Parameters::Set resolve( const CE::Rendering::Parameters::PassSemantics& pass_semantics, const CE::Rendering::Parameters::DrawSemantics& draw_semantics, const CE::Rendering::Parameters::Set& pass_values, diff --git a/projects/engine/include/cheryl/assets/resources/resource-provider.h b/projects/engine/include/cheryl/assets/resources/resource-provider.h index c44a4713..d70db54b 100644 --- a/projects/engine/include/cheryl/assets/resources/resource-provider.h +++ b/projects/engine/include/cheryl/assets/resources/resource-provider.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "decoded-image.h" #include "geometry2d.h" #include "image.h" @@ -32,7 +34,7 @@ namespace CE::Rendering::Resources { * requires their original live backend domain. Final release follows backend * retirement; it is not a promise of immediate native deletion. */ - struct Provider { + struct CE_API Provider { private: const CE::Diagnostics::DomainId domain_ = CE::Diagnostics::next_domain_id(); diff --git a/projects/engine/include/cheryl/assets/resources/sampler.h b/projects/engine/include/cheryl/assets/resources/sampler.h index 9c58e54f..4100ca28 100644 --- a/projects/engine/include/cheryl/assets/resources/sampler.h +++ b/projects/engine/include/cheryl/assets/resources/sampler.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -22,11 +24,11 @@ namespace CE::Rendering::Sampling { }; // CPU-only validation of the requested filtering and wrap modes. - void validate(const Options& options); + CE_API void validate(const Options& options); // Immutable sampling in one backend domain. Bindings retain the sampler // independently of image pixels; final release follows backend retirement. - class Sampler { + class CE_API Sampler { const Options options_; const float effective_anisotropy_; diff --git a/projects/engine/include/cheryl/assets/resources/shader-asset-builder.h b/projects/engine/include/cheryl/assets/resources/shader-asset-builder.h index 601d901e..f5c3d771 100644 --- a/projects/engine/include/cheryl/assets/resources/shader-asset-builder.h +++ b/projects/engine/include/cheryl/assets/resources/shader-asset-builder.h @@ -1,12 +1,14 @@ #pragma once +#include + #include #include namespace CE::Assets::Shaders { // Optional provider-owned capability. Validation is CPU-only; construction // runs on that provider's loading owner and returns complete native candidates. - struct Builder { + struct CE_API Builder { virtual ~Builder() = default; virtual void validate_program(const ProgramRecipe& recipe) const = 0; virtual void validate_material(const MaterialRecipe& recipe, const ProgramRecipe& program) const = 0; diff --git a/projects/engine/include/cheryl/assets/resources/shader.h b/projects/engine/include/cheryl/assets/resources/shader.h index 02f73ee7..67daac89 100644 --- a/projects/engine/include/cheryl/assets/resources/shader.h +++ b/projects/engine/include/cheryl/assets/resources/shader.h @@ -1,5 +1,7 @@ #pragma once +#include + #include namespace CE::Rendering::Parameters { @@ -22,7 +24,7 @@ namespace CE::Rendering::Resources { // Mutable executable-program access on the backend owner/current context. // Retain the program through use; uniform names/matrices are borrowed for each // call. Native type/name validation and failure state belong to the backend. - struct Program { + struct CE_API Program { virtual ~Program() = default; // Bind semantic engine parameters; concrete backends choose their representation. virtual void bind_pass(const CE::Rendering::Parameters::PassSemantics& pass) = 0; diff --git a/projects/engine/include/cheryl/assets/selection/tile-selection.h b/projects/engine/include/cheryl/assets/selection/tile-selection.h index 45a26f60..8c7ed068 100644 --- a/projects/engine/include/cheryl/assets/selection/tile-selection.h +++ b/projects/engine/include/cheryl/assets/selection/tile-selection.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -49,7 +51,7 @@ namespace CE::Assets::Tilesets::Selection { // Missing/empty candidate lists return MissingRule; unresolved required samples // return IncompleteNeighborhood. Invalid options/rule metadata or an empty sampler // throw invalid_args; sampler exceptions propagate. No grid or animation lookup here. - [[nodiscard]] Result select(const WangRule& rule, const Sampler& sampler, const Options& options); - [[nodiscard]] Result select(const BitmaskRule& rule, const Sampler& sampler, const Options& options); - [[nodiscard]] Result select(const Rule& rule, const Sampler& sampler, const Options& options); + [[nodiscard]] CE_API Result select(const WangRule& rule, const Sampler& sampler, const Options& options); + [[nodiscard]] CE_API Result select(const BitmaskRule& rule, const Sampler& sampler, const Options& options); + [[nodiscard]] CE_API Result select(const Rule& rule, const Sampler& sampler, const Options& options); } diff --git a/projects/engine/include/cheryl/assets/submission/draw2d.h b/projects/engine/include/cheryl/assets/submission/draw2d.h index 353d4453..3a054135 100644 --- a/projects/engine/include/cheryl/assets/submission/draw2d.h +++ b/projects/engine/include/cheryl/assets/submission/draw2d.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -38,7 +40,7 @@ namespace CE::Rendering::Submission { * nonempty and absent from style.parameters. * @return Owned packet ready to add to a render pass; native use requires its live resource domain. */ - [[nodiscard]] DrawPacket2D resolve_sprite( + [[nodiscard]] CE_API DrawPacket2D resolve_sprite( const CE::Assets::Sprites::Asset& sprite, CE::Assets::Grids::CellIndex cell, const DrawStyle2D& style, @@ -47,20 +49,21 @@ namespace CE::Rendering::Submission { // CPU-only resolution from stable inputs: copies values and retains resources, // without binding or advancing playback. Failures publish no frame packets. // Cell overloads check grid bounds; all returned packets validate material/ranges. - [[nodiscard]] DrawPacket2D resolve_tile( + [[nodiscard]] CE_API DrawPacket2D resolve_tile( const CE::Assets::Tilesets::Asset& tileset, CE::Assets::Grids::CellIndex cell, const DrawStyle2D& style, const Context2D& context ); - [[nodiscard]] DrawPacket2D resolve_tile(const CE::Assets::Tilesets::Tile& tile, const DrawStyle2D& style, const Context2D& context); - [[nodiscard]] DrawPacket2D + [[nodiscard]] CE_API DrawPacket2D + resolve_tile(const CE::Assets::Tilesets::Tile& tile, const DrawStyle2D& style, const Context2D& context); + [[nodiscard]] CE_API DrawPacket2D resolve_tile(const CE::Assets::Tilesets::Animation& animation, const DrawStyle2D& style, const Context2D& context); - [[nodiscard]] DrawPacket2D + [[nodiscard]] CE_API DrawPacket2D resolve_graphic(const CE::Assets::Graphics::Asset& graphic, const DrawStyle2D& style, const Context2D& context); // Layout does not retain text. Glyph offsets are scaled then transformed by the // caller's model; packets retain the font resources. Empty layout returns no draws. - [[nodiscard]] std::vector resolve_text( + [[nodiscard]] CE_API std::vector resolve_text( const CE::Text::Legacy::Font& font, std::string_view text, const DrawStyle2D& style, @@ -79,7 +82,7 @@ namespace CE::Rendering::Submission { * @return Owned packets in authored glyph order, empty when there are no glyph draws; * native use requires the original live resource domain. */ - [[nodiscard]] std::vector + [[nodiscard]] CE_API std::vector resolve_text(const CE::Text::Rendering::Generation& text, const DrawStyle2D& style, const Context2D& context); // Explicitly requires positioned_runs preparation and a shader applying the // common scale/model transform to positioned vertices, without per-glyph or @@ -87,6 +90,6 @@ namespace CE::Rendering::Submission { // the caller's model applies to the whole text. Other shaders use resolve_text. // Regrouped floating-point transforms can change boundary pixels. Use // resolve_text when the original per-glyph raster evaluation is required. - [[nodiscard]] std::vector + [[nodiscard]] CE_API std::vector resolve_text_runs(const CE::Text::Rendering::Generation& text, const DrawStyle2D& style, const Context2D& context); } diff --git a/projects/engine/include/cheryl/assets/types/2d/ffont.h b/projects/engine/include/cheryl/assets/types/2d/ffont.h index a14b760b..2683460d 100644 --- a/projects/engine/include/cheryl/assets/types/2d/ffont.h +++ b/projects/engine/include/cheryl/assets/types/2d/ffont.h @@ -1,4 +1,6 @@ #pragma once + +#include #ifndef FFONT_H #define FFONT_H @@ -16,6 +18,7 @@ namespace CE::Rendering::Resources { } namespace CE::Text::Legacy { + struct CE_TYPE_API BitmapFont; constexpr uint16_t bitmap_character_count = 256; using BitmapFontData = std::tuple< std::array, @@ -37,14 +40,31 @@ namespace CE::Text::Legacy { explicit BitmapFont(const BitmapFontData& data) : Font({std::get<1>(data), std::get<2>(data)}), widths(std::get<0>(data)) {} ~BitmapFont() override = default; - [[nodiscard]] std::vector layout(std::string_view text, LayoutOptions options = LayoutOptions{}) const override; + [[nodiscard]] CE_API std::vector + layout(std::string_view text, LayoutOptions options = LayoutOptions{}) const override; /** Read 256 native shorts from a binary input file before any provider upload. * Incomplete reads fail; legacy trailing bytes are ignored. Metadata/endian * semantics remain unverified; see docs/resources/legacy-ffont.md for * the deprecation decision and preserved legacy behavior. */ - static BitmapFontData load_ffont(const std::filesystem::path& path, CE::Rendering::Resources::Provider& provider); + CE_API static BitmapFontData load_ffont(const std::filesystem::path& path, CE::Rendering::Resources::Provider& provider); }; } +#if defined(_MSC_VER) +#pragma warning(push) +#pragma warning(disable : 4996) +#elif defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; +} +#if defined(_MSC_VER) +#pragma warning(pop) +#elif defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + #endif diff --git a/projects/engine/include/cheryl/assets/types/2d/font.h b/projects/engine/include/cheryl/assets/types/2d/font.h index 5dbdaf11..c73e6012 100644 --- a/projects/engine/include/cheryl/assets/types/2d/font.h +++ b/projects/engine/include/cheryl/assets/types/2d/font.h @@ -1,4 +1,6 @@ #pragma once + +#include #ifndef CEFONT_H #define CEFONT_H #include "base/asset2d.h" @@ -24,10 +26,10 @@ namespace CE::Text::Legacy { /** Retains glyph geometry/atlas; direct base construction does not validate them. * Immutable implementations support CPU layout without a graphics context. */ - struct Font : protected CE::Assets::Content2D { + struct CE_TYPE_API Font : protected CE::Assets::Content2D { explicit Font(const FontResources& data) : CE::Assets::Content2D(std::get<0>(data), std::get<1>(data)) {} - virtual ~Font() = default; + CE_API virtual ~Font(); // Returns owned placements without retaining text. Encoding/metrics and // option support belong to the implementation; this interface promises no shaping. [[nodiscard]] virtual std::vector layout(std::string_view text, LayoutOptions options = LayoutOptions{}) const = 0; diff --git a/projects/engine/include/cheryl/assets/types/2d/graphic.h b/projects/engine/include/cheryl/assets/types/2d/graphic.h index db2eb8b2..dba127e4 100644 --- a/projects/engine/include/cheryl/assets/types/2d/graphic.h +++ b/projects/engine/include/cheryl/assets/types/2d/graphic.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -21,13 +23,13 @@ namespace CE::Assets::Graphics { // Upload on the provider owner with a compatible image domain. Null/empty // images throw; no cache entry is published, and CPU vertices are transient. - [[nodiscard]] static Asset from_image( + [[nodiscard]] CE_API static Asset from_image( std::shared_ptr image, CE::Rendering::Resources::Provider& provider, CE::Rendering::Geometry::Pivot pivot = CE::Rendering::Geometry::Pivot{0.0f, 0.0f} ); // Decode then upload on that same owner; failure returns no CE::Assets::Graphics::Asset. - [[nodiscard]] static Asset load( + [[nodiscard]] CE_API static Asset load( const std::filesystem::path& file, CE::Rendering::Resources::Provider& provider, CE::Rendering::Geometry::Pivot pivot = CE::Rendering::Geometry::Pivot{0.0f, 0.0f} diff --git a/projects/engine/include/cheryl/assets/types/2d/sprite.h b/projects/engine/include/cheryl/assets/types/2d/sprite.h index 6b34a645..ff360872 100644 --- a/projects/engine/include/cheryl/assets/types/2d/sprite.h +++ b/projects/engine/include/cheryl/assets/types/2d/sprite.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -24,21 +26,21 @@ namespace CE::Assets::Sprites { */ class Animation final { public: - Animation& operator[](std::size_t frame); + CE_API Animation& operator[](std::size_t frame); // Wrap looping clips, clamp nonlooping clips, and reset time within the frame. - void set_frame(std::size_t frame); + CE_API void set_frame(std::size_t frame); // Finite nonnegative seconds; invalid time throws without changing the cursor. // Exact duration boundaries advance; nonlooping clips stop at their last frame. - void advance(std::chrono::duration elapsed); + CE_API void advance(std::chrono::duration elapsed); [[nodiscard]] std::size_t index() const { return index_; } - [[nodiscard]] CE::Assets::Grids::CellIndex cell() const; + [[nodiscard]] CE_API CE::Assets::Grids::CellIndex cell() const; [[nodiscard]] const Clip& definition() const { return *definition_; } - [[nodiscard]] std::chrono::milliseconds frame_duration() const; + [[nodiscard]] CE_API std::chrono::milliseconds frame_duration() const; [[nodiscard]] bool loops() const { return definition_->loop; } private: friend struct Asset; - explicit Animation(std::shared_ptr definition); + CE_API explicit Animation(std::shared_ptr definition); std::shared_ptr definition_; std::size_t index_ = 0; @@ -51,20 +53,20 @@ namespace CE::Assets::Sprites { * fields or backend compatibility. Read concurrently only while inputs are stable. */ struct Asset final : Content2D { - explicit Asset(Data data); + CE_API explicit Asset(Data data); - Animation operator[](const std::string& animation) const; + CE_API Animation operator[](const std::string& animation) const; // Prefer an exact name/facing. Without a facing, one matching clip is allowed; // missing/ambiguous requests throw. operator[] uses this faceless lookup. - Animation animation(const std::string& animation, std::optional facing = std::nullopt) const; - [[nodiscard]] bool has_animation(const std::string& animation, std::optional facing = std::nullopt) const; + CE_API Animation animation(const std::string& animation, std::optional facing = std::nullopt) const; + [[nodiscard]] CE_API bool has_animation(const std::string& animation, std::optional facing = std::nullopt) const; // Metadata references borrow this CE::Assets::Sprites::Asset's lifetime; missing names throw out_of_range. - [[nodiscard]] const CE::Assets::Grids::View& view(const std::string& name) const; - [[nodiscard]] CE::Assets::Grids::CellIndex orientation(const std::string& name) const; + [[nodiscard]] CE_API const CE::Assets::Grids::View& view(const std::string& name) const; + [[nodiscard]] CE_API CE::Assets::Grids::CellIndex orientation(const std::string& name) const; [[nodiscard]] const Definition& definition() const { return *definition_; } private: - [[nodiscard]] static std::string animation_key(const std::string& animation, const std::optional& facing); + [[nodiscard]] CE_API static std::string animation_key(const std::string& animation, const std::optional& facing); std::shared_ptr definition_; std::unordered_map animation_indices_; diff --git a/projects/engine/include/cheryl/assets/types/2d/stbfont.h b/projects/engine/include/cheryl/assets/types/2d/stbfont.h index 649dddff..15de487e 100644 --- a/projects/engine/include/cheryl/assets/types/2d/stbfont.h +++ b/projects/engine/include/cheryl/assets/types/2d/stbfont.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -31,24 +33,25 @@ namespace CE::Text::Legacy { /** Baked ASCII glyph quads, advances, and alpha atlas. Layout reads immutable * metrics, so published text commands can share a font without changing it. */ - struct AsciiFont final : Font { + struct CE_TYPE_API AsciiFont final : Font { private: std::array advances_{}; float line_height_{}; public: - explicit AsciiFont(AsciiFontData data); + CE_API explicit AsciiFont(AsciiFontData data); ~AsciiFont() override = default; // Baked pixel offsets; newline moves down one line, CR is ignored, tab advances // four spaces, other unsupported UTF-8 scalars or malformed subparts select '?'. // This ASCII atlas has no shaping or non-ASCII glyphs. alternate_bank throws. - [[nodiscard]] std::vector layout(std::string_view text, LayoutOptions options = LayoutOptions{}) const override; + [[nodiscard]] CE_API std::vector + layout(std::string_view text, LayoutOptions options = LayoutOptions{}) const override; /** Bake printable ASCII from a caller-supplied font file at a positive * font_size in pixels. Upload obeys provider affinity; failure returns no data * and publishes no cache entry. System discovery is optional; bundled fonts * use this same path. The first face is selected for a collection. */ - [[nodiscard]] static AsciiFontData + [[nodiscard]] CE_API static AsciiFontData load_font(const std::filesystem::path& font_path, int font_size, CE::Rendering::Resources::Provider& provider); [[nodiscard]] const CE::Rendering::Geometry::Buffer2D& glyph_geometry() const { return *geometry; } diff --git a/projects/engine/include/cheryl/assets/types/2d/tileset.h b/projects/engine/include/cheryl/assets/types/2d/tileset.h index 54751a36..0c643e88 100644 --- a/projects/engine/include/cheryl/assets/types/2d/tileset.h +++ b/projects/engine/include/cheryl/assets/types/2d/tileset.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -44,7 +46,7 @@ namespace CE::Assets::Tilesets { Clip definition_; public: - explicit Animation( + CE_API explicit Animation( Clip definition, const CE::Memory::Shared& geometry, const CE::Memory::Shared& texture, @@ -52,7 +54,7 @@ namespace CE::Assets::Tilesets { ); [[nodiscard]] const Clip& definition() const { return definition_; } - [[nodiscard]] std::chrono::milliseconds frame_duration() const; + [[nodiscard]] CE_API std::chrono::milliseconds frame_duration() const; [[nodiscard]] bool loops() const { return definition_.loop; } }; @@ -67,29 +69,30 @@ namespace CE::Assets::Tilesets { std::unordered_map animation_targets_; public: - explicit Asset(Data data); + CE_API explicit Asset(Data data); // Retained resource values; tile() rejects out-of-grid cells. animation() throws // out_of_range for an unknown name; animation_for() returns no value for no target. - [[nodiscard]] Tile tile(std::size_t cell) const; - [[nodiscard]] Animation animation(const std::string& name) const; - [[nodiscard]] std::optional animation_for(std::size_t target) const; + [[nodiscard]] CE_API Tile tile(std::size_t cell) const; + [[nodiscard]] CE_API Animation animation(const std::string& name) const; + [[nodiscard]] CE_API std::optional animation_for(std::size_t target) const; // Resolve only the original target's clip; frame cells never recurse. Negative // time or invalid timelines throw invalid_args; original/final grid violations // throw bad_request. Static targets still require nonnegative time. - [[nodiscard]] CE::Assets::Grids::CellIndex cell_at(CE::Assets::Grids::CellIndex target, std::chrono::milliseconds elapsed) const; + [[nodiscard]] CE_API CE::Assets::Grids::CellIndex + cell_at(CE::Assets::Grids::CellIndex target, std::chrono::milliseconds elapsed) const; // Unknown names throw out_of_range. Preserve selector failure values, otherwise // apply cell_at() once. No callback is retained; submit the returned cell value. - [[nodiscard]] CE::Assets::Tilesets::Selection::Result select_tile( + [[nodiscard]] CE_API CE::Assets::Tilesets::Selection::Result select_tile( const std::string& name, const CE::Assets::Tilesets::Selection::Sampler& sampler, const CE::Assets::Tilesets::Selection::Options& options, std::chrono::milliseconds elapsed ) const; // References borrow this CE::Assets::Tilesets::Asset's lifetime; unknown metadata names throw out_of_range. - [[nodiscard]] const CE::Assets::Grids::View& view(const std::string& name) const; - [[nodiscard]] CE::Assets::Grids::CellIndex orientation(const std::string& name) const; - [[nodiscard]] const Rule& autotile(const std::string& name) const; + [[nodiscard]] CE_API const CE::Assets::Grids::View& view(const std::string& name) const; + [[nodiscard]] CE_API CE::Assets::Grids::CellIndex orientation(const std::string& name) const; + [[nodiscard]] CE_API const Rule& autotile(const std::string& name) const; [[nodiscard]] const Definition& definition() const { return definition_; } }; } diff --git a/projects/engine/include/cheryl/assets/types/2d/unicode-text.h b/projects/engine/include/cheryl/assets/types/2d/unicode-text.h index fd3b9152..0d20b644 100644 --- a/projects/engine/include/cheryl/assets/types/2d/unicode-text.h +++ b/projects/engine/include/cheryl/assets/types/2d/unicode-text.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -51,9 +53,9 @@ namespace CE::Text::Rendering { std::vector runs_; bool positioned_runs_ = false; - explicit Prepared(ShapedText layout); - friend Prepared prepare(ShapedText, RasterOptions); - friend Generation upload(Prepared, CE::Rendering::Resources::Provider&); + CE_API explicit Prepared(ShapedText layout); + friend CE_API Prepared prepare(ShapedText, RasterOptions); + friend CE_API Generation upload(Prepared, CE::Rendering::Resources::Provider&); public: [[nodiscard]] const ShapedText& layout() const noexcept { return layout_; } @@ -78,7 +80,7 @@ namespace CE::Text::Rendering { std::vector runs_; bool positioned_runs_ = false; - Generation( + CE_API Generation( ShapedText layout, std::vector pages, std::vector draws, @@ -86,7 +88,7 @@ namespace CE::Text::Rendering { std::vector runs, bool positioned_runs ); - friend Generation upload(Prepared, CE::Rendering::Resources::Provider&); + friend CE_API Generation upload(Prepared, CE::Rendering::Resources::Provider&); public: [[nodiscard]] const ShapedText& layout() const noexcept { return layout_; } @@ -107,7 +109,7 @@ namespace CE::Text::Rendering { * @param options Maximum atlas-page extent and optional positioned-run geometry. * @return Complete CPU preparation ready for upload_text(), with no native resources. */ - [[nodiscard]] Prepared prepare(ShapedText layout, RasterOptions options = {}); + [[nodiscard]] CE_API Prepared prepare(ShapedText layout, RasterOptions options = {}); /** Upload one immutable text generation on the provider's owner/current context. * Failure returns no generation; earlier successful transient uploads retire * through their backend rather than an atomic native rollback. Mismatched @@ -117,5 +119,5 @@ namespace CE::Text::Rendering { * @param provider Backend receiving the atlas and geometry uploads, borrowed for this call. * @return Owned layout and resource handles; native use retains the provider-domain limits. */ - [[nodiscard]] Generation upload(Prepared prepared, CE::Rendering::Resources::Provider& provider); + [[nodiscard]] CE_API Generation upload(Prepared prepared, CE::Rendering::Resources::Provider& provider); } diff --git a/projects/engine/include/cheryl/audio/clip.h b/projects/engine/include/cheryl/audio/clip.h index 4632cb8c..ea0fd3a4 100644 --- a/projects/engine/include/cheryl/audio/clip.h +++ b/projects/engine/include/cheryl/audio/clip.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -12,7 +14,7 @@ namespace CE::Audio { std::uint32_t sample_rate = 48000; // Initial scope: mono/stereo, 8–192 kHz. Throws for unsupported values. - void validate() const; + CE_API void validate() const; bool operator==(const Format&) const = default; }; @@ -25,17 +27,17 @@ namespace CE::Audio { struct Storage; std::shared_ptr storage_; - explicit Clip(std::shared_ptr storage); + CE_API explicit Clip(std::shared_ptr storage); public: Clip(const Clip&) = default; Clip& operator=(const Clip&) = default; // Rejects empty data, incomplete frames and non-finite samples. - [[nodiscard]] static Clip from_samples(::CE::Audio::Format format, std::vector samples); - [[nodiscard]] ::CE::Audio::Format format() const noexcept; - [[nodiscard]] std::uint64_t frame_count() const noexcept; - [[nodiscard]] std::chrono::duration duration() const noexcept; - [[nodiscard]] std::span samples() const noexcept; + [[nodiscard]] CE_API static Clip from_samples(::CE::Audio::Format format, std::vector samples); + [[nodiscard]] CE_API ::CE::Audio::Format format() const noexcept; + [[nodiscard]] CE_API std::uint64_t frame_count() const noexcept; + [[nodiscard]] CE_API std::chrono::duration duration() const noexcept; + [[nodiscard]] CE_API std::span samples() const noexcept; }; } diff --git a/projects/engine/include/cheryl/audio/system.h b/projects/engine/include/cheryl/audio/system.h index 1e28040f..a1dc6c96 100644 --- a/projects/engine/include/cheryl/audio/system.h +++ b/projects/engine/include/cheryl/audio/system.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "clip.h" #include @@ -14,7 +16,7 @@ namespace CE::Audio { bool looping = false; bool paused = false; - void validate() const; + CE_API void validate() const; }; struct VoiceSnapshot { @@ -25,7 +27,7 @@ namespace CE::Audio { }; // Normalized linear gain, inclusive [0, 1]; rejects non-finite values. - void validate_volume(float volume); + CE_API void validate_volume(float volume); /** One independently controlled playback. Controls and snapshots are synchronized. * Pause preserves progress; resume affects only Paused voices. Stop is terminal @@ -35,7 +37,7 @@ namespace CE::Audio { * A handle may survive its system: snapshot then reports Closed, and controls throw. * Releasing a handle does not stop an active voice; the system retains playback. */ - class Voice { + class CE_API Voice { public: virtual ~Voice() = default; [[nodiscard]] virtual ::CE::Audio::VoiceSnapshot snapshot() const = 0; @@ -57,7 +59,7 @@ namespace CE::Audio { * The application settles producers before destroying this object. Surviving * Voice handles remain safe. Subsequent system operations throw, except closed/close. */ - class System { + class CE_API System { public: virtual ~System() = default; [[nodiscard]] virtual std::shared_ptr<::CE::Audio::Voice> diff --git a/projects/engine/include/cheryl/core/controls/input-accumulator.h b/projects/engine/include/cheryl/core/controls/input-accumulator.h index 876de737..f29058f7 100644 --- a/projects/engine/include/cheryl/core/controls/input-accumulator.h +++ b/projects/engine/include/cheryl/core/controls/input-accumulator.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "poll-snapshot.h" #include "tick-input.h" @@ -24,7 +26,7 @@ namespace CE::Input { return input; } - [[nodiscard]] Tick consume_polls(ObservationClock::time_point until, std::vector> polls); + [[nodiscard]] CE_API Tick consume_polls(ObservationClock::time_point until, std::vector> polls); private: std::shared_ptr previous_; diff --git a/projects/engine/include/cheryl/core/controls/input-bindings.h b/projects/engine/include/cheryl/core/controls/input-bindings.h index 041b704c..5771ef09 100644 --- a/projects/engine/include/cheryl/core/controls/input-bindings.h +++ b/projects/engine/include/cheryl/core/controls/input-bindings.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "action-snapshot.h" #include "device-binding.h" @@ -63,11 +65,11 @@ namespace CE::Input::Actions { std::unordered_map pending_buttons_; std::atomic> published_{std::make_shared()}; - [[nodiscard]] bool chord_active(const Chord& chord) const; - [[nodiscard]] bool button_active(Id action) const; - [[nodiscard]] std::unordered_map evaluate_buttons() const; - [[nodiscard]] std::unordered_map evaluate_axes() const; - void refresh_button(Id action); + [[nodiscard]] CE_API bool chord_active(const Chord& chord) const; + [[nodiscard]] CE_API bool button_active(Id action) const; + [[nodiscard]] CE_API std::unordered_map evaluate_buttons() const; + [[nodiscard]] CE_API std::unordered_map evaluate_axes() const; + CE_API void refresh_button(Id action); public: /** Bind one physical control to a semantic button on the polling owner. @@ -77,7 +79,7 @@ namespace CE::Input::Actions { * @param action Application-owned semantic identity read from tick input. * @return Nonzero registration ID for later unbind(). */ - [[nodiscard]] BindingId bind_button(CE::Input::Control control, Id action); + [[nodiscard]] CE_API BindingId bind_button(CE::Input::Control control, Id action); /** Bind an AND chord to a semantic button; alternative mappings combine with OR. * Empty chords and conflicting button/axis use of an action are rejected. * Configuration belongs to the polling owner. @@ -85,38 +87,38 @@ namespace CE::Input::Actions { * @param action Application-owned semantic button identity. * @return Nonzero registration ID for later unbind(). */ - [[nodiscard]] BindingId bind_button(Chord chord, Id action); + [[nodiscard]] CE_API BindingId bind_button(Chord chord, Id action); // Several axis bindings contribute additively; a modifier chord gates its axis. // Scale/dead zone must be finite. Empty modifier chords are unconditional. - [[nodiscard]] BindingId bind_axis(CE::Input::Control axis, Id action, AxisOptions options = {}); - [[nodiscard]] BindingId bind_axis(Chord modifiers, CE::Input::Control axis, Id action, AxisOptions options = {}); + [[nodiscard]] CE_API BindingId bind_axis(CE::Input::Control axis, Id action, AxisOptions options = {}); + [[nodiscard]] CE_API BindingId bind_axis(Chord modifiers, CE::Input::Control axis, Id action, AxisOptions options = {}); /** Remove one mapping on the polling owner. * Removal affects the next publication; it does not erase already mapped * relative activity or earlier immutable handles. * @param binding Registration ID returned when the mapping was added. * @return Whether an existing mapping was removed; unknown IDs return false. */ - bool unbind(BindingId binding); - void unbind_action(Id action); + CE_API bool unbind(BindingId binding); + CE_API void unbind_action(Id action); // Drop mappings/physical staging and publish release/zero State from the old baseline. - void clear(); + CE_API void clear(); // Platform-owned routing gate. Physical state is retained while semantic // actions are suppressed, so other devices and alternative mappings survive. - void set_device_enabled(CE::Input::DeviceId device, bool enabled); + CE_API void set_device_enabled(CE::Input::DeviceId device, bool enabled); // Backends report finite physical values; values are not clamped to [-1, 1]. // Prior values are tracked here. Staging mutations are not transactional on failure. - void on_axis(CE::Input::Control binding, float value); + CE_API void on_axis(CE::Input::Control binding, float value); // Relative motion accumulates until publication; it does not persist into later polls. - void on_delta(CE::Input::Control binding, float delta); - void on_button(CE::Input::Control binding, bool held); + CE_API void on_delta(CE::Input::Control binding, float delta); + CE_API void on_button(CE::Input::Control binding, bool held); // Call after one poll with nondecreasing observation time. Complete before // atomic publication; failure keeps the previous published handle. Success // clears pending transitions/relative activity. Poll IDs may skip on failure. - [[nodiscard]] std::shared_ptr + [[nodiscard]] CE_API std::shared_ptr publish_actions(std::chrono::steady_clock::time_point observed_at = std::chrono::steady_clock::now()); // Atomic retained-handle read from any thread, including before the first poll. - [[nodiscard]] std::shared_ptr action_snapshot() const; + [[nodiscard]] CE_API std::shared_ptr action_snapshot() const; }; } // namespace CE::Input::Actions diff --git a/projects/engine/include/cheryl/core/controls/input-capture.h b/projects/engine/include/cheryl/core/controls/input-capture.h index c0637815..d4d3fef7 100644 --- a/projects/engine/include/cheryl/core/controls/input-capture.h +++ b/projects/engine/include/cheryl/core/controls/input-capture.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "input-record.h" #include "input-routing.h" @@ -25,15 +27,15 @@ namespace CE::Input::Capture { public: Lease() = default; - ~Lease(); - Lease(Lease&& other) noexcept; - Lease& operator=(Lease&& other) noexcept; + CE_API ~Lease(); + CE_API Lease(Lease&& other) noexcept; + CE_API Lease& operator=(Lease&& other) noexcept; Lease(const Lease&) = delete; Lease& operator=(const Lease&) = delete; - void reset() noexcept; + CE_API void reset() noexcept; private: - Lease(std::shared_ptr counts, Channel mode); + CE_API Lease(std::shared_ptr counts, Channel mode); std::shared_ptr counts_; Channel mode_ = Channel::State; }; @@ -48,19 +50,19 @@ namespace CE::Input::Capture { Collector(const Collector&) = delete; Collector& operator=(const Collector&) = delete; // Concurrent requests are supported; invalid modes throw. State is a no-op request. - [[nodiscard]] Lease request(Channel mode); - void begin_poll(CE::Input::Routing::Focus focus = {}); + [[nodiscard]] CE_API Lease request(Channel mode); + CE_API void begin_poll(CE::Input::Routing::Focus focus = {}); // Owns data in collector order for active channels only. Captured Text must // be a Unicode scalar; observation timestamps and other payloads are not normalized. - void record( + CE_API void record( CE::Input::DeviceId device, CE::Input::DeviceKind kind, CE::Input::Events::Payload data, CE::Input::ObservationClock::time_point observed_at = CE::Input::ObservationClock::now() ); // Transfer the whole pending vector; discard does not revoke outstanding leases. - [[nodiscard]] std::vector complete(); - void discard_pending(); + [[nodiscard]] CE_API std::vector complete(); + CE_API void discard_pending(); private: std::shared_ptr counts_ = std::make_shared(); diff --git a/projects/engine/include/cheryl/core/controls/input-interface.h b/projects/engine/include/cheryl/core/controls/input-interface.h index 526ff998..04506295 100644 --- a/projects/engine/include/cheryl/core/controls/input-interface.h +++ b/projects/engine/include/cheryl/core/controls/input-interface.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "input-bindings.h" #include "input-capture.h" #include "poll-snapshot.h" @@ -18,9 +20,9 @@ namespace CE::Input { * Bindings, device queries and polling belong to the platform owner; capture/ * focus requests and acquired immutable handles may cross threads. */ - class System { + class CE_TYPE_API System { public: - virtual ~System() = default; + CE_API virtual ~System(); virtual void initialize(CE::Platform::Window& window) = 0; virtual void poll() = 0; virtual void deinitialize() = 0; @@ -34,20 +36,21 @@ namespace CE::Input { [[nodiscard]] virtual bool supports_focus() const { return false; } // Unsupported channels/focus throw failed_operation. Leases retain request // state, not the adapter/window, and never consume a private record queue. - [[nodiscard]] Capture::Lease capture(Capture::Channel mode); - [[nodiscard]] Routing::Manager& routing(); + [[nodiscard]] CE_API Capture::Lease capture(Capture::Channel mode); + [[nodiscard]] CE_API Routing::Manager& routing(); // Retrieve after completed publication on the platform owner. State-only // adapters get empty records; capture-capable State-only publication throws. - [[nodiscard]] virtual std::shared_ptr poll_snapshot(); + [[nodiscard]] CE_API virtual std::shared_ptr poll_snapshot(); protected: // Before pumping, latch one focus/capture snapshot for routing and State gating. - void begin_input_poll(); + CE_API void begin_input_poll(); [[nodiscard]] Capture::Collector& capture_buffer() { return capture_; } - [[nodiscard]] std::shared_ptr publish_input(ObservationClock::time_point observed_at = ObservationClock::now()); + [[nodiscard]] CE_API std::shared_ptr + publish_input(ObservationClock::time_point observed_at = ObservationClock::now()); // Detachment cleanup drops pending records/focus, preserving delivered handles // and live capture request counts. Collector staging has no rollback guarantee. - void discard_captured_input(); + CE_API void discard_captured_input(); private: Capture::Collector capture_; diff --git a/projects/engine/include/cheryl/core/controls/input-routing.h b/projects/engine/include/cheryl/core/controls/input-routing.h index 92cae60f..c2aaf14d 100644 --- a/projects/engine/include/cheryl/core/controls/input-routing.h +++ b/projects/engine/include/cheryl/core/controls/input-routing.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "input-types.h" #include @@ -30,19 +32,19 @@ namespace CE::Input::Routing { public: Lease() = default; - ~Lease(); - Lease(Lease&& other) noexcept; - Lease& operator=(Lease&& other) noexcept; + CE_API ~Lease(); + CE_API Lease(Lease&& other) noexcept; + CE_API Lease& operator=(Lease&& other) noexcept; Lease(const Lease&) = delete; Lease& operator=(const Lease&) = delete; - void reset() noexcept; + CE_API void reset() noexcept; // Snapshot only; ownership can change immediately after this observation. - [[nodiscard]] bool owns_focus() const; + [[nodiscard]] CE_API bool owns_focus() const; [[nodiscard]] TargetId target() const { return focus_ ? focus_->target : 0; } [[nodiscard]] std::uint64_t epoch() const { return focus_ ? focus_->epoch : 0; } private: - Lease(std::shared_ptr state, std::shared_ptr focus); + CE_API Lease(std::shared_ptr state, std::shared_ptr focus); std::shared_ptr state_; std::shared_ptr focus_; }; @@ -58,7 +60,7 @@ namespace CE::Input::Routing { Manager& operator=(const Manager&) = delete; // Nonzero target/valid policy required; atomic publication wins among concurrent // requests. Epoch identifies a request, not a widget's lifetime or capture lease. - [[nodiscard]] Lease focus(TargetId target, KeyboardPolicy routing = KeyboardPolicy::Exclusive); + [[nodiscard]] CE_API Lease focus(TargetId target, KeyboardPolicy routing = KeyboardPolicy::Exclusive); [[nodiscard]] std::shared_ptr current() const { return state_->current.load(); } // Clear current ownership without changing capture activation or published records. void clear() { state_->current.store(state_->empty); } diff --git a/projects/engine/include/cheryl/core/controls/polling-backlog.h b/projects/engine/include/cheryl/core/controls/polling-backlog.h index 16add9d0..f2dcbcbe 100644 --- a/projects/engine/include/cheryl/core/controls/polling-backlog.h +++ b/projects/engine/include/cheryl/core/controls/polling-backlog.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "poll-snapshot.h" #include @@ -22,20 +24,20 @@ namespace CE::Input { class PollingBacklog final { public: // Finite capacity must be positive; spacing nonnegative and policy known. - explicit PollingBacklog(PollingOptions options = {}); + CE_API explicit PollingBacklog(PollingOptions options = {}); - [[nodiscard]] bool can_poll() const; - [[nodiscard]] bool poll_due(ObservationClock::time_point now) const; + [[nodiscard]] CE_API bool can_poll() const; + [[nodiscard]] CE_API bool poll_due(ObservationClock::time_point now) const; // max() when capacity is full; consumption restores capacity without resetting spacing. - [[nodiscard]] ObservationClock::time_point next_poll_at() const; + [[nodiscard]] CE_API ObservationClock::time_point next_poll_at() const; [[nodiscard]] std::size_t completed_polls() const { return polls_.size(); } // Requires eligible spacing/capacity, nonnull poll/State, a newer poll ID, // and observation time no later than completion. Invalid inputs throw; // allocation failure is not a rollback/retry guarantee for mutable staging. - void complete(std::shared_ptr poll, ObservationClock::time_point completed_at); - void complete(std::shared_ptr state, ObservationClock::time_point completed_at); + CE_API void complete(std::shared_ptr poll, ObservationClock::time_point completed_at); + CE_API void complete(std::shared_ptr state, ObservationClock::time_point completed_at); // Transfers the whole batch and immediately leaves a fresh empty backlog. - [[nodiscard]] std::vector> consume(); + [[nodiscard]] CE_API std::vector> consume(); private: PollingOptions options_; diff --git a/projects/engine/include/cheryl/core/controls/tick-input.h b/projects/engine/include/cheryl/core/controls/tick-input.h index e638fa03..c8960930 100644 --- a/projects/engine/include/cheryl/core/controls/tick-input.h +++ b/projects/engine/include/cheryl/core/controls/tick-input.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "action-snapshot.h" #include "input-record.h" @@ -54,8 +56,8 @@ namespace CE::Input { // nondecreasing by observation time, and no later than until. since must be // at/after the baseline and until at/after since; invalid input throws. // For manual consumers, the interval defaults to the sample timestamps. - Tick(std::shared_ptr previous, std::vector> polls); - Tick( + CE_API Tick(std::shared_ptr previous, std::vector> polls); + CE_API Tick( std::shared_ptr previous, std::vector> polls, ObservationClock::time_point since, @@ -64,11 +66,11 @@ namespace CE::Input { // Owned state copies. axis() rejects a kind change within this poll batch; // changing kind between consumptions is supported. Durations remain seconds. - [[nodiscard]] Actions::ButtonTickState button(Actions::Id action) const; - [[nodiscard]] Actions::AxisTickState axis(Actions::Id action) const; + [[nodiscard]] CE_API Actions::ButtonTickState button(Actions::Id action) const; + [[nodiscard]] CE_API Actions::AxisTickState axis(Actions::Id action) const; [[nodiscard]] std::span> polls() const { return polls_; } // Retained final sample, or the baseline if this batch is empty. - [[nodiscard]] std::shared_ptr latest_poll() const; + [[nodiscard]] CE_API std::shared_ptr latest_poll() const; [[nodiscard]] ObservationDuration elapsed() const { return until_ - since_; } // Shared, non-destructive view for every consumer in this simulation update. [[nodiscard]] std::span records() const { return records_; } diff --git a/projects/engine/include/cheryl/core/diagnostics.h b/projects/engine/include/cheryl/core/diagnostics.h index e25113b9..aa6e2aa9 100644 --- a/projects/engine/include/cheryl/core/diagnostics.h +++ b/projects/engine/include/cheryl/core/diagnostics.h @@ -1,5 +1,7 @@ #pragma once +#include + #include namespace CE::Diagnostics { @@ -7,7 +9,7 @@ namespace CE::Diagnostics { // Process-local, monotonic identities for diagnostics, never addresses/native // handles. Zero means exhausted/unavailable; IDs are not persistence keys. - [[nodiscard]] DomainId next_domain_id() noexcept; + [[nodiscard]] CE_API DomainId next_domain_id() noexcept; // Best-effort cumulative observations, not a scheduling/admission contract. struct DispatchStats { diff --git a/projects/engine/include/cheryl/core/display/display-system-interface.h b/projects/engine/include/cheryl/core/display/display-system-interface.h index 871f0d5b..eed81534 100644 --- a/projects/engine/include/cheryl/core/display/display-system-interface.h +++ b/projects/engine/include/cheryl/core/display/display-system-interface.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "monitor.h" #include "window-interface.h" @@ -12,7 +14,7 @@ namespace CE::Platform { * Monitor references are borrowed inventory snapshots with backend-defined refresh * and invalidation points. Copy values before handing them to another thread. */ - class DisplaySystem { + class CE_API DisplaySystem { public: virtual ~DisplaySystem() = default; /** Borrow the backend inventory; references expire on its refresh or destruction. */ diff --git a/projects/engine/include/cheryl/core/display/monitor.h b/projects/engine/include/cheryl/core/display/monitor.h index d37af7b7..5da91bf3 100644 --- a/projects/engine/include/cheryl/core/display/monitor.h +++ b/projects/engine/include/cheryl/core/display/monitor.h @@ -1,4 +1,6 @@ #pragma once + +#include #include "viewport.h" #include @@ -9,7 +11,7 @@ namespace CE::Platform { * Native lookup remains subject to the originating display's inventory lifetime. */ struct Monitor : Dimensions { - Monitor(std::uint64_t id, int width, int height); + CE_API Monitor(std::uint64_t id, int width, int height); Monitor(const Monitor&) = default; // Inventory identity, not an OS handle or persistent hardware identifier. [[nodiscard]] std::uint64_t id() const { return id_; } diff --git a/projects/engine/include/cheryl/core/display/window-interface.h b/projects/engine/include/cheryl/core/display/window-interface.h index fbed2fcb..8a1795e7 100644 --- a/projects/engine/include/cheryl/core/display/window-interface.h +++ b/projects/engine/include/cheryl/core/display/window-interface.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "framebuffer-size.h" #include "viewport.h" #include @@ -12,7 +14,7 @@ namespace CE::Platform { * Queries do not synchronize with another thread. Copy observed values into the * runtime handoff instead of reading a live window from simulation/render workers. */ - class Window { + class CE_API Window { public: virtual ~Window() = default; [[nodiscard]] virtual ::CE::Platform::Dimensions logical_size() const = 0; diff --git a/projects/engine/include/cheryl/core/engine/engine-context.h b/projects/engine/include/cheryl/core/engine/engine-context.h index 69cb173c..6b625b30 100644 --- a/projects/engine/include/cheryl/core/engine/engine-context.h +++ b/projects/engine/include/cheryl/core/engine/engine-context.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "platform-dispatcher.h" #include "worker-pool.h" @@ -69,7 +71,7 @@ namespace CE::Runtime { bool destroying_ = false; public: - Context( + CE_API Context( std::unique_ptr display, std::unique_ptr surface, std::unique_ptr renderer, @@ -77,7 +79,7 @@ namespace CE::Runtime { CE::Input::System& input, ExecutionOptions execution = ExecutionOptions{} ); - Context( + CE_API Context( std::unique_ptr display, std::unique_ptr surface, std::unique_ptr renderer, @@ -85,33 +87,33 @@ namespace CE::Runtime { std::unique_ptr input, ExecutionOptions execution = ExecutionOptions{} ); - ~Context(); + CE_API ~Context(); Context(const Context&) = delete; Context& operator=(const Context&) = delete; // Borrowed adapters, not ownership transfer; getters do not marshal work. // window() requires the display owner and throws if there is no active window. - [[nodiscard]] CE::Platform::DisplaySystem& display() const; - [[nodiscard]] CE::Platform::Window& window() const; - [[nodiscard]] CE::Rendering::PresentationSurface& surface() const; - [[nodiscard]] CE::Rendering::Renderer& renderer() const; - [[nodiscard]] CE::Rendering::Resources::Provider& resources() const; - [[nodiscard]] CE::Input::System& input() const; + [[nodiscard]] CE_API CE::Platform::DisplaySystem& display() const; + [[nodiscard]] CE_API CE::Platform::Window& window() const; + [[nodiscard]] CE_API CE::Rendering::PresentationSurface& surface() const; + [[nodiscard]] CE_API CE::Rendering::Renderer& renderer() const; + [[nodiscard]] CE_API CE::Rendering::Resources::Provider& resources() const; + [[nodiscard]] CE_API CE::Input::System& input() const; [[nodiscard]] PlatformDispatcher& platform_dispatcher() { return platform_dispatcher_; } [[nodiscard]] CE::Diagnostics::DomainId diagnostic_id() const noexcept { return domain_; } // Groups created here are part of this context's shutdown domain, even // when their physical capacity comes from an application-supplied pool. // Synchronized across producers; closed context/pool or invalid options throw. - [[nodiscard]] CE::Workers::Group make_worker_group(CE::Workers::GroupOptions options = CE::Workers::GroupOptions{}); + [[nodiscard]] CE_API CE::Workers::Group make_worker_group(CE::Workers::GroupOptions options = CE::Workers::GroupOptions{}); private: friend class GameRuntime; friend struct Detail::ContextAccess; - void validate() const; - void begin_session(); - void close_worker_submissions(); - [[nodiscard]] bool workers_idle() const; - void finish_workers(); + CE_API void validate() const; + CE_API void begin_session(); + CE_API void close_worker_submissions(); + [[nodiscard]] CE_API bool workers_idle() const; + CE_API void finish_workers(); }; } diff --git a/projects/engine/include/cheryl/core/engine/event-delivery.h b/projects/engine/include/cheryl/core/engine/event-delivery.h index 3167112d..a142948c 100644 --- a/projects/engine/include/cheryl/core/engine/event-delivery.h +++ b/projects/engine/include/cheryl/core/engine/event-delivery.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "platform-dispatcher.h" #include "simulation-dispatcher.h" #include "worker-pool.h" @@ -10,9 +12,9 @@ namespace CE::Events { * dispatch services and remains immediate by default. Accepted task cancellation * is reported by EventBus's owned delivery ticket, not a discarded future. */ - [[nodiscard]] EventBus::Delivery platform_delivery(CE::Runtime::PlatformDispatcher::Submission endpoint); - [[nodiscard]] EventBus::Delivery simulation_delivery(CE::Runtime::SimulationDispatcher::Submission endpoint); + [[nodiscard]] CE_API EventBus::Delivery platform_delivery(CE::Runtime::PlatformDispatcher::Submission endpoint); + [[nodiscard]] CE_API EventBus::Delivery simulation_delivery(CE::Runtime::SimulationDispatcher::Submission endpoint); // One serial stream on shared CE::Workers::Group capacity. Copies of the returned // callable share ordering; separate calls create independently concurrent streams. - [[nodiscard]] EventBus::Delivery worker_delivery(CE::Workers::Group group); + [[nodiscard]] CE_API EventBus::Delivery worker_delivery(CE::Workers::Group group); } diff --git a/projects/engine/include/cheryl/core/engine/platform-dispatcher.h b/projects/engine/include/cheryl/core/engine/platform-dispatcher.h index d0878fae..3cb35750 100644 --- a/projects/engine/include/cheryl/core/engine/platform-dispatcher.h +++ b/projects/engine/include/cheryl/core/engine/platform-dispatcher.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -86,7 +88,7 @@ namespace CE::Runtime { }; PlatformDispatcher() = default; - ~PlatformDispatcher(); + CE_API ~PlatformDispatcher(); PlatformDispatcher(const PlatformDispatcher&) = delete; PlatformDispatcher& operator=(const PlatformDispatcher&) = delete; @@ -96,16 +98,16 @@ namespace CE::Runtime { return submission().submit(std::forward(work)); } // Synchronized queued-work snapshot; false does not establish completion/closure. - [[nodiscard]] bool has_pending() const; + [[nodiscard]] CE_API bool has_pending() const; // Queue snapshot only. Callback exceptions remain owned by their futures. - [[nodiscard]] CE::Diagnostics::DispatchStats diagnostics() const; + [[nodiscard]] CE_API CE::Diagnostics::DispatchStats diagnostics() const; private: - static void enqueue(const std::shared_ptr& state, Task request); - static void require_owner(const State& state); - void open(std::function wake); - void drain(Context& engine); - void close(); - void invalidate(); + CE_API static void enqueue(const std::shared_ptr& state, Task request); + CE_API static void require_owner(const State& state); + CE_API void open(std::function wake); + CE_API void drain(Context& engine); + CE_API void close(); + CE_API void invalidate(); }; } diff --git a/projects/engine/include/cheryl/core/engine/simulation-dispatcher.h b/projects/engine/include/cheryl/core/engine/simulation-dispatcher.h index f91f43f6..21e6866e 100644 --- a/projects/engine/include/cheryl/core/engine/simulation-dispatcher.h +++ b/projects/engine/include/cheryl/core/engine/simulation-dispatcher.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -86,7 +88,7 @@ namespace CE::Runtime { }; SimulationDispatcher() = default; - ~SimulationDispatcher(); + CE_API ~SimulationDispatcher(); SimulationDispatcher(const SimulationDispatcher&) = delete; SimulationDispatcher& operator=(const SimulationDispatcher&) = delete; @@ -95,17 +97,17 @@ namespace CE::Runtime { return submission().submit(std::forward(work)); } // Synchronized queued-work snapshot; false does not establish completion/closure. - [[nodiscard]] bool has_pending() const; + [[nodiscard]] CE_API bool has_pending() const; // Queue snapshot only. Callback exceptions remain owned by their futures. - [[nodiscard]] CE::Diagnostics::DispatchStats diagnostics() const; + [[nodiscard]] CE_API CE::Diagnostics::DispatchStats diagnostics() const; private: - static void enqueue(const std::shared_ptr& state, Task request); - static void require_owner(const State& state); - void open(std::function wake); - void bind_owner(); - void drain(); - void close(); - void invalidate(); + CE_API static void enqueue(const std::shared_ptr& state, Task request); + CE_API static void require_owner(const State& state); + CE_API void open(std::function wake); + CE_API void bind_owner(); + CE_API void drain(); + CE_API void close(); + CE_API void invalidate(); }; } diff --git a/projects/engine/include/cheryl/core/engine/worker-configuration.h b/projects/engine/include/cheryl/core/engine/worker-configuration.h index d20fcd4f..d2b72b9a 100644 --- a/projects/engine/include/cheryl/core/engine/worker-configuration.h +++ b/projects/engine/include/cheryl/core/engine/worker-configuration.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -15,6 +17,6 @@ namespace CE::Workers { // Names start with a lowercase ASCII letter and contain lowercase letters, // digits and underscores. Configuration is inert until the caller creates work. - [[nodiscard]] bool is_group_name(std::string_view name) noexcept; - void validate_configuration(const Configuration& configuration); + [[nodiscard]] CE_API bool is_group_name(std::string_view name) noexcept; + CE_API void validate_configuration(const Configuration& configuration); } diff --git a/projects/engine/include/cheryl/core/engine/worker-pool.h b/projects/engine/include/cheryl/core/engine/worker-pool.h index 689b275c..09666b87 100644 --- a/projects/engine/include/cheryl/core/engine/worker-pool.h +++ b/projects/engine/include/cheryl/core/engine/worker-pool.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -72,7 +74,7 @@ namespace CE::Workers { // Common structural validation for direct C++ options and configuration loaders. // Does not discover topology, create threads, or apply native policy. - void validate_group_options(const GroupOptions& options); + CE_API void validate_group_options(const GroupOptions& options); struct GroupPolicy { GroupOptions requested; @@ -114,9 +116,9 @@ namespace CE::Workers { std::weak_ptr pool_; std::shared_ptr group_; - Group(std::shared_ptr pool, std::shared_ptr group); - void enqueue(Detail::Job job) const; - static void record_failure(const std::weak_ptr& group) noexcept; + CE_API Group(std::shared_ptr pool, std::shared_ptr group); + CE_API void enqueue(Detail::Job job) const; + CE_API static void record_failure(const std::weak_ptr& group) noexcept; public: // Owns the callable/captures until execution or policy failure. Closure/pool @@ -147,14 +149,14 @@ namespace CE::Workers { return result; } - void close() const; - void drain() const; + CE_API void close() const; + CE_API void drain() const; // Owned snapshots; status is observation only, policy is immutable after creation. - [[nodiscard]] GroupStatus status() const; - [[nodiscard]] GroupPolicy policy() const; + [[nodiscard]] CE_API GroupStatus status() const; + [[nodiscard]] CE_API GroupPolicy policy() const; // Explicit observer: caller holds no scheduler/context/application lock. // Callback failures remain future-owned; policy degradation is coalesced. - void report_diagnostics() const noexcept; + CE_API void report_diagnostics() const noexcept; }; /** Independently owned reusable CPU workers. One thread is the modest default; @@ -169,28 +171,28 @@ namespace CE::Workers { std::vector workers_; std::mutex shutdown_mutex_; - Pool(std::size_t worker_count, Detail::NativeAdapter adapter); - Pool(PoolOptions options, Detail::NativeAdapter adapter); + CE_API Pool(std::size_t worker_count, Detail::NativeAdapter adapter); + CE_API Pool(PoolOptions options, Detail::NativeAdapter adapter); public: // Positive physical thread count; partial thread-start failure joins created threads. - explicit Pool(std::size_t worker_count = 1); - explicit Pool(PoolOptions options); - ~Pool(); + CE_API explicit Pool(std::size_t worker_count = 1); + CE_API explicit Pool(PoolOptions options); + CE_API ~Pool(); Pool(const Pool&) = delete; Pool& operator=(const Pool&) = delete; // Thread-safe publication; invalid options or unavailable required policy throw. // Preferred policy can fall back as reported by the returned group's policy(). - [[nodiscard]] Group make_group(GroupOptions options = GroupOptions{}); + [[nodiscard]] CE_API Group make_group(GroupOptions options = GroupOptions{}); [[nodiscard]] std::size_t worker_count() const { return workers_.size(); } // Owned startup snapshot, not a live query of changing OS eligibility. - [[nodiscard]] Capabilities capabilities() const; - [[nodiscard]] Topology topology() const; - [[nodiscard]] CE::Diagnostics::DomainId diagnostic_id() const noexcept; + [[nodiscard]] CE_API Capabilities capabilities() const; + [[nodiscard]] CE_API Topology topology() const; + [[nodiscard]] CE_API CE::Diagnostics::DomainId diagnostic_id() const noexcept; // Explicit observer; constructors/shutdown do not log under an outer owner lock. - void report_diagnostics() const noexcept; - void close(); - void shutdown(); + CE_API void report_diagnostics() const noexcept; + CE_API void close(); + CE_API void shutdown(); }; } diff --git a/projects/engine/include/cheryl/core/engine/worker-topology.h b/projects/engine/include/cheryl/core/engine/worker-topology.h index 44516c07..0f3dfd60 100644 --- a/projects/engine/include/cheryl/core/engine/worker-topology.h +++ b/projects/engine/include/cheryl/core/engine/worker-topology.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -32,7 +34,7 @@ namespace CE::Workers { std::string limitations; }; - class TopologyProvider { + class CE_API TopologyProvider { public: virtual ~TopologyProvider() = default; // Owned discovery snapshot. Methods must support concurrent pool workers. diff --git a/projects/engine/include/cheryl/core/game-framework/abstract-game.h b/projects/engine/include/cheryl/core/game-framework/abstract-game.h index f366df31..da946830 100644 --- a/projects/engine/include/cheryl/core/game-framework/abstract-game.h +++ b/projects/engine/include/cheryl/core/game-framework/abstract-game.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include "tick-context.h" @@ -19,7 +21,7 @@ namespace CE::Game { * worker. The game must not change input bindings or upload GPU resources from the worker. * Hook failures stop the session; runtime cleanup preserves the first failure. */ - struct AbstractGame { + struct CE_API AbstractGame { virtual ~AbstractGame() = default; /** Initialize game state after rendering and input adapters are ready. * Register bindings and upload resources on the platform/graphics owner. diff --git a/projects/engine/include/cheryl/core/game-framework/game-runtime.h b/projects/engine/include/cheryl/core/game-framework/game-runtime.h index 586fa266..3ce76afd 100644 --- a/projects/engine/include/cheryl/core/game-framework/game-runtime.h +++ b/projects/engine/include/cheryl/core/game-framework/game-runtime.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -88,7 +90,7 @@ namespace CE::Runtime { std::function)> simulation_thread_factory_; public: - GameRuntime( + CE_API GameRuntime( ::CE::Runtime::Context& engine, ::CE::Game::AbstractGame& game, ::CE::Runtime::RunMode mode = ::CE::Runtime::RunMode::Sequential, @@ -99,25 +101,25 @@ namespace CE::Runtime { // One blocking session on the platform owner. Borrowed engine/game must remain // alive through return/throw; cleanup preserves the first failure. Do not destroy // the runtime while run() or another operation is using it. - void run(); + CE_API void run(); // Thread-safe request; run() performs normal teardown after simulation stops. - void stop(); + CE_API void stop(); // Caller synchronizes with run start/return. Query before run or after // its return/throw; live sampling rejects. Counters publish after join. - [[nodiscard]] ::CE::Runtime::RuntimeStats diagnostics() const; + [[nodiscard]] CE_API ::CE::Runtime::RuntimeStats diagnostics() const; [[nodiscard]] ::CE::Runtime::SimulationDispatcher& simulation_dispatcher() { return simulation_dispatcher_; } private: friend struct ::CE::Runtime::Detail::GameRuntimeAccess; - void run_sequential(); - void run_concurrent(); - void finish_unstarted_session(); - void pump_shutdown_requests(std::exception_ptr& failure); - void finish_worker_shutdown(std::exception_ptr& failure); - void preserve_failure(std::exception_ptr& first, const char* phase, std::exception_ptr next); - void report_session(bool failed) const noexcept; - void observe_input(const std::shared_ptr& snapshot) noexcept; - void observe_timing(const ::CE::Runtime::Simulation::Batch& batch) noexcept; - void observe_platform(bool pressure) noexcept; + CE_API void run_sequential(); + CE_API void run_concurrent(); + CE_API void finish_unstarted_session(); + CE_API void pump_shutdown_requests(std::exception_ptr& failure); + CE_API void finish_worker_shutdown(std::exception_ptr& failure); + CE_API void preserve_failure(std::exception_ptr& first, const char* phase, std::exception_ptr next); + CE_API void report_session(bool failed) const noexcept; + CE_API void observe_input(const std::shared_ptr& snapshot) noexcept; + CE_API void observe_timing(const ::CE::Runtime::Simulation::Batch& batch) noexcept; + CE_API void observe_platform(bool pressure) noexcept; }; } diff --git a/projects/engine/include/cheryl/core/game-framework/simulation-scheduler.h b/projects/engine/include/cheryl/core/game-framework/simulation-scheduler.h index cfd2db07..3f06a25b 100644 --- a/projects/engine/include/cheryl/core/game-framework/simulation-scheduler.h +++ b/projects/engine/include/cheryl/core/game-framework/simulation-scheduler.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -48,13 +50,13 @@ namespace CE::Runtime::Simulation { ::CE::Runtime::Simulation::Clock::duration accumulated_{}; public: - explicit Scheduler( + CE_API explicit Scheduler( ::CE::Runtime::Simulation::TimingOptions options = ::CE::Runtime::Simulation::TimingOptions{}, ::CE::Runtime::Simulation::Clock::time_point start = ::CE::Runtime::Simulation::Clock::now() ); [[nodiscard]] const ::CE::Runtime::Simulation::TimingOptions& options() const { return options_; } - [[nodiscard]] ::CE::Runtime::Simulation::Batch advance(::CE::Runtime::Simulation::Clock::time_point now); - [[nodiscard]] ::CE::Runtime::Simulation::Clock::time_point next_update_at() const; + [[nodiscard]] CE_API ::CE::Runtime::Simulation::Batch advance(::CE::Runtime::Simulation::Clock::time_point now); + [[nodiscard]] CE_API ::CE::Runtime::Simulation::Clock::time_point next_update_at() const; }; } diff --git a/projects/engine/include/cheryl/core/game-framework/tick-context.h b/projects/engine/include/cheryl/core/game-framework/tick-context.h index 55e9f151..d1474b94 100644 --- a/projects/engine/include/cheryl/core/game-framework/tick-context.h +++ b/projects/engine/include/cheryl/core/game-framework/tick-context.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -30,10 +32,10 @@ namespace CE::Game { * without one reject the operation. This does not end update(): return * explicitly when the game should stop processing the current tick. */ - void request_stop() const; + CE_API void request_stop() const; [[nodiscard]] double observed_seconds() const { return input.elapsed().count(); } // Derived control policy: observed down-time proportion times simulation delta. // With no observation interval, use held State. This does not recover tap history. - [[nodiscard]] double button_simulation_seconds(::CE::Input::Actions::Id action) const; + [[nodiscard]] CE_API double button_simulation_seconds(::CE::Input::Actions::Id action) const; }; } diff --git a/projects/engine/include/cheryl/core/logging.h b/projects/engine/include/cheryl/core/logging.h index b4e53569..211d2c43 100644 --- a/projects/engine/include/cheryl/core/logging.h +++ b/projects/engine/include/cheryl/core/logging.h @@ -6,5 +6,5 @@ #ifdef ST_ON_SIGNALS #include -extern backward::SignalHandling sh; +extern CE_API backward::SignalHandling sh; #endif diff --git a/projects/engine/include/cheryl/core/logging/backend-guard.h b/projects/engine/include/cheryl/core/logging/backend-guard.h index 9090b4ea..fd6b5b64 100644 --- a/projects/engine/include/cheryl/core/logging/backend-guard.h +++ b/projects/engine/include/cheryl/core/logging/backend-guard.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -43,25 +45,25 @@ namespace CE::Logging::Detail { BackendCounters* previous_; public: - explicit BackendScope(BackendCounters& counters) noexcept; - ~BackendScope(); + CE_API explicit BackendScope(BackendCounters& counters) noexcept; + CE_API ~BackendScope(); BackendScope(const BackendScope&) = delete; BackendScope& operator=(const BackendScope&) = delete; }; // Suppress ordinary writes; reject operations that could wait on this backend. // Both run before singleton initialization or lifecycle/delegate locks. - [[nodiscard]] bool suppress_backend_emission() noexcept; - void reject_backend_reentry(const char* operation); - [[nodiscard]] bool in_backend() noexcept; + [[nodiscard]] CE_API bool suppress_backend_emission() noexcept; + CE_API void reject_backend_reentry(const char* operation); + [[nodiscard]] CE_API bool in_backend() noexcept; struct QueueReports { std::atomic_size_t reported_discards{0}; }; - void report_queue_loss(const std::shared_ptr& reports, std::size_t discards) noexcept; - void report_backend_summary(const char* name, const std::shared_ptr& counters) noexcept; - [[nodiscard]] std::shared_ptr make_owned_pool(const std::shared_ptr& reports); + CE_API void report_queue_loss(const std::shared_ptr& reports, std::size_t discards) noexcept; + CE_API void report_backend_summary(const char* name, const std::shared_ptr& counters) noexcept; + [[nodiscard]] CE_API std::shared_ptr make_owned_pool(const std::shared_ptr& reports); class DestinationState final { const std::shared_ptr counters_; @@ -71,14 +73,14 @@ namespace CE::Logging::Detail { std::array close_failures_{}; public: - DestinationState(std::shared_ptr counters, bool file); - [[nodiscard]] bool degraded() const noexcept; - void suppress() noexcept; - void fail_operation(bool flush, std::exception_ptr failure) noexcept; - void fail_open_callback() noexcept; - void fail_close_callback(std::size_t slot, std::exception_ptr failure) noexcept; + CE_API DestinationState(std::shared_ptr counters, bool file); + [[nodiscard]] CE_API bool degraded() const noexcept; + CE_API void suppress() noexcept; + CE_API void fail_operation(bool flush, std::exception_ptr failure) noexcept; + CE_API void fail_open_callback() noexcept; + CE_API void fail_close_callback(std::size_t slot, std::exception_ptr failure) noexcept; // Called only after delegate locks have unwound, or after destruction. - void report_close_failures() noexcept; + CE_API void report_close_failures() noexcept; [[nodiscard]] BackendCounters& counters() const noexcept { return *counters_; } }; @@ -91,51 +93,50 @@ namespace CE::Logging::Detail { * retained-file ownership. Backend operations use this guard; direct delegate * log/flush and native sink-graph replacement bypass the supported contract. */ - class GuardedSink final : public spdlog::sinks::sink { + class CE_TYPE_API GuardedSink final : public spdlog::sinks::sink { const spdlog::sink_ptr delegate_; const std::shared_ptr state_; public: - GuardedSink(spdlog::sink_ptr delegate, std::shared_ptr state); - [[nodiscard]] bool admits(spdlog::level level) const noexcept; - void log(const spdlog::details::log_msg& message) override; - void flush() override; - void set_pattern(const std::string& pattern) override; - void set_formatter(std::unique_ptr formatter) override; + CE_API GuardedSink(spdlog::sink_ptr delegate, std::shared_ptr state); + [[nodiscard]] CE_API bool admits(spdlog::level level) const noexcept; + CE_API void log(const spdlog::details::log_msg& message) override; + CE_API void flush() override; + CE_API void set_pattern(const std::string& pattern) override; + CE_API void set_formatter(std::unique_ptr formatter) override; }; /** Published native owners submit through this frontend. The private async * backend retains the same guarded destinations and borrows the pool. The * sink vector is fixed; changing it is rejected rather than bypassing guards. */ - class OwnedLogger final : public spdlog::logger { + class CE_TYPE_API OwnedLogger final : public spdlog::logger { const std::vector owned_sinks_; const std::shared_ptr counters_; const std::weak_ptr pool_; const spdlog::async_overflow_policy overflow_; const std::shared_ptr backend_; - [[nodiscard]] bool valid_graph() const noexcept; + [[nodiscard]] CE_API bool valid_graph() const noexcept; public: - OwnedLogger( + CE_API OwnedLogger( std::string name, const std::vector& sinks, std::weak_ptr pool, spdlog::async_overflow_policy overflow, std::shared_ptr counters ); - ~OwnedLogger() override; + CE_API ~OwnedLogger() override; // Clones retain the fixed graph and the same guarded submission boundary. - std::shared_ptr clone(std::string name) override; + CE_API std::shared_ptr clone(std::string name) override; protected: - void sink_it_(const spdlog::details::log_msg& message) override; - void flush_() override; + CE_API void sink_it_(const spdlog::details::log_msg& message) override; + CE_API void flush_() override; }; - [[nodiscard]] spdlog::file_event_handlers guard_file_handlers( - const spdlog::file_event_handlers& handlers, const std::shared_ptr& state - ); - [[nodiscard]] bool sink_admits(const spdlog::sink_ptr& sink, spdlog::level level) noexcept; + [[nodiscard]] CE_API spdlog::file_event_handlers + guard_file_handlers(const spdlog::file_event_handlers& handlers, const std::shared_ptr& state); + [[nodiscard]] CE_API bool sink_admits(const spdlog::sink_ptr& sink, spdlog::level level) noexcept; } diff --git a/projects/engine/include/cheryl/core/logging/log-names.h b/projects/engine/include/cheryl/core/logging/log-names.h index b183a224..b88bde26 100644 --- a/projects/engine/include/cheryl/core/logging/log-names.h +++ b/projects/engine/include/cheryl/core/logging/log-names.h @@ -1,15 +1,17 @@ #pragma once +#include + namespace CE::Logging::Categories { // Engine records select these names explicitly, independently of the host's // default logger. Each used category owns its rotating file and console gate. - extern const char engine[]; // "engine": runtime, execution and events. - extern const char platform[]; // "os-platform": OS window, display and input services. - extern const char rendering[]; // "rendering": rendering, shaders and native graphics resources. - extern const char assets[]; // "assets": preparation, publication and reload. - extern const char memory[]; // "memory": allocator bookkeeping and ownership. + extern CE_API const char engine[]; // "engine": runtime, execution and events. + extern CE_API const char platform[]; // "os-platform": OS window, display and input services. + extern CE_API const char rendering[]; // "rendering": rendering, shaders and native graphics resources. + extern CE_API const char assets[]; // "assets": preparation, publication and reload. + extern CE_API const char memory[]; // "memory": allocator bookkeeping and ownership. // Explicit legacy destination for existing CE::Logging::CherylLog/Logger users. Engine-owned // operations use the named categories above; they do not create cheryl.log. - extern const char cheryl[]; // "cheryl". + extern CE_API const char cheryl[]; // "cheryl". } diff --git a/projects/engine/include/cheryl/core/logging/log.h b/projects/engine/include/cheryl/core/logging/log.h index 6d0dceb2..dc2efe9b 100644 --- a/projects/engine/include/cheryl/core/logging/log.h +++ b/projects/engine/include/cheryl/core/logging/log.h @@ -1,8 +1,11 @@ #pragma once + +#include #include "osink.h" #include "compile-policy.h" #include "config.h" #include "backend-guard.h" +#include "log-names.h" #include #include @@ -27,9 +30,9 @@ #include namespace CE::Logging::Detail { - inline std::mutex default_logger_mutex; + extern CE_API std::mutex default_logger_mutex; - [[nodiscard]] uint16_t next_log_id() noexcept; + [[nodiscard]] CE_API uint16_t next_log_id() noexcept; enum class LogState { Opening, Open, Closing, Closed }; @@ -75,7 +78,7 @@ namespace CE::Logging::Detail { namespace CE::Diagnostics { // Bounded trace; empty if resolution or copying fails. Never masks the caller's failure. - extern std::string stack_trace(void* addr0 = nullptr) noexcept; + CE_API extern std::string stack_trace(void* addr0 = nullptr) noexcept; } namespace CE::Logging { @@ -270,6 +273,16 @@ namespace CE::Logging::Detail { }; } +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; + template <> CE_API auto Singleton>::storage() -> Storage&; + template <> CE_API auto Singleton>::storage() -> Storage&; + template <> CE_API auto Singleton>::storage() -> Storage&; + template <> CE_API auto Singleton>::storage() -> Storage&; + template <> CE_API auto Singleton>::storage() -> Storage&; + template <> CE_API auto Singleton>::storage() -> Storage&; +} + // template definitions - methods namespace CE::Logging { template void Log::construct_log() { diff --git a/projects/engine/include/cheryl/core/rendering/camera.h b/projects/engine/include/cheryl/core/rendering/camera.h index f70fb699..e758befb 100644 --- a/projects/engine/include/cheryl/core/rendering/camera.h +++ b/projects/engine/include/cheryl/core/rendering/camera.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -11,16 +13,16 @@ namespace CE::Rendering { * Mutable state is unsynchronized: use one owner and copy matrices into frames. * Matrix references borrow this camera and must not race with its setters. */ - class CameraBase { + class CE_TYPE_API CameraBase { public: - virtual ~CameraBase() = default; + CE_API virtual ~CameraBase(); // Pixel dimensions; negatives throw unchanged. Zero sizes are retained but // projection uses at least one pixel per axis. Equal sizes keep the revision. - void set_framebuffer_size(::CE::Platform::FramebufferSize size); + CE_API void set_framebuffer_size(::CE::Platform::FramebufferSize size); // Exact component equality suppresses updates. No finiteness/invertibility // validation; changed matrices advance the revision once. - void set_view_matrix(const glm::mat4& view); + CE_API void set_view_matrix(const glm::mat4& view); [[nodiscard]] ::CE::Platform::FramebufferSize framebuffer_size() const { return framebuffer_size_; } [[nodiscard]] const glm::mat4& projection_matrix() const { return projection_matrix_; } @@ -37,24 +39,24 @@ namespace CE::Rendering { }; // Y-up orthographic bounds [0, width] x [0, height], near/far 0/1 using GLM. - class Camera2D final : public CameraBase { + class CE_TYPE_API Camera2D final : public CameraBase { public: - Camera2D(); + CE_API Camera2D(); protected: - void recalculate_projection() override; + CE_API void recalculate_projection() override; }; // GLM perspective using framebuffer aspect; construction starts at revision zero. - class Camera3D final : public CameraBase { + class CE_TYPE_API Camera3D final : public CameraBase { public: - Camera3D(); + CE_API Camera3D(); // Vertical FOV in finite degrees (0, 180), finite 0 < near < far in view // units. Invalid values throw unchanged; a changed configuration advances once. - void set_perspective(float fov_degrees, float near_plane, float far_plane); + CE_API void set_perspective(float fov_degrees, float near_plane, float far_plane); protected: - void recalculate_projection() override; + CE_API void recalculate_projection() override; private: float fov_degrees_ = 45.0f; diff --git a/projects/engine/include/cheryl/core/rendering/clip-region.h b/projects/engine/include/cheryl/core/rendering/clip-region.h index 7a3a7fc1..7a9ad333 100644 --- a/projects/engine/include/cheryl/core/rendering/clip-region.h +++ b/projects/engine/include/cheryl/core/rendering/clip-region.h @@ -1,5 +1,7 @@ #pragma once +#include + #include namespace CE::Rendering { @@ -33,9 +35,9 @@ namespace CE::Rendering { bool operator==(const PixelClipRect2D&) const = default; }; - void validate_clip_region(const ClipRegion2D& region); - [[nodiscard]] ClipRect2D intersect_clip_rects(const ClipRect2D& first, const ClipRect2D& second); + CE_API void validate_clip_region(const ClipRegion2D& region); + [[nodiscard]] CE_API ClipRect2D intersect_clip_rects(const ClipRect2D& first, const ClipRect2D& second); // Clamp to the logical viewport, then round nonempty edges outwards: floor // left/top, ceil right/bottom. Empty clips and zero-sized framebuffers draw nothing. - [[nodiscard]] PixelClipRect2D resolve_clip_region(const ClipRegion2D& region, ::CE::Platform::FramebufferSize framebuffer); + [[nodiscard]] CE_API PixelClipRect2D resolve_clip_region(const ClipRegion2D& region, ::CE::Platform::FramebufferSize framebuffer); } diff --git a/projects/engine/include/cheryl/core/rendering/draw-packet.h b/projects/engine/include/cheryl/core/rendering/draw-packet.h index 5910a4f1..c78a58ef 100644 --- a/projects/engine/include/cheryl/core/rendering/draw-packet.h +++ b/projects/engine/include/cheryl/core/rendering/draw-packet.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -39,9 +41,9 @@ namespace CE::Rendering { // CPU-only validation of clip, handles, geometry range/state and complete // parameters. Native domain/context compatibility is checked during playback. - void validate_draw_packet(const DrawPacket2D& packet, const ::CE::Rendering::Pipelines::PassConstraints2D& constraints); + CE_API void validate_draw_packet(const DrawPacket2D& packet, const ::CE::Rendering::Pipelines::PassConstraints2D& constraints); // Stable inputs only; throws without modifying them or publishing a frame packet. - [[nodiscard]] DrawPacket2D resolve_draw_packet( + [[nodiscard]] CE_API DrawPacket2D resolve_draw_packet( std::shared_ptr geometry, std::size_t first_vertex, std::size_t vertex_count, diff --git a/projects/engine/include/cheryl/core/rendering/presentation-surface.h b/projects/engine/include/cheryl/core/rendering/presentation-surface.h index abfa88ee..bfbb2442 100644 --- a/projects/engine/include/cheryl/core/rendering/presentation-surface.h +++ b/projects/engine/include/cheryl/core/rendering/presentation-surface.h @@ -1,12 +1,14 @@ #pragma once +#include + namespace CE::Rendering { /** Presents a completed frame on a window owned by the display adapter. * A graphics backend can extend this interface with its context requirements. * Call on the platform/presentation owner while that borrowed window is alive; * presentation does not publish simulation state or retain a CE::Rendering::Frame. */ - class PresentationSurface { + class CE_API PresentationSurface { public: virtual ~PresentationSurface() = default; virtual void present() = 0; diff --git a/projects/engine/include/cheryl/core/rendering/renderer.h b/projects/engine/include/cheryl/core/rendering/renderer.h index 714b1bf4..d4c5c846 100644 --- a/projects/engine/include/cheryl/core/rendering/renderer.h +++ b/projects/engine/include/cheryl/core/rendering/renderer.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -13,7 +15,7 @@ namespace CE::Rendering { * brackets native use with backend initialization/teardown and retains its context. * A backend failure may leave native state changed or part of a frame drawn. */ - class Renderer { + class CE_API Renderer { public: virtual ~Renderer() = default; diff --git a/projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h b/projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h index 7722764f..7f9ba433 100644 --- a/projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h +++ b/projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -64,7 +66,7 @@ namespace CE::Assets::Loading { mutable std::mutex observations_mutex_; UploadStats last_upload_; - [[nodiscard]] Prepared prepare_selected(const std::vector& indexes, bool all_images) const; + [[nodiscard]] CE_API Prepared prepare_selected(const std::vector& indexes, bool all_images) const; public: /** Configure an owned loader without discovering files or uploading resources. @@ -78,7 +80,7 @@ namespace CE::Assets::Loading { * failure leaves caches and this loader's retained metadata unchanged. * @return Owned definitions, shader sources, pixels and file discovery for upload(). */ - [[nodiscard]] Prepared prepare() const; + [[nodiscard]] CE_API Prepared prepare() const; /** Prepare selected graphics indexes against full-root discovery. * Decode only referenced images; a shader-only selection decodes none. * Empty selections and discovery, decoding or validation failures reject @@ -86,7 +88,7 @@ namespace CE::Assets::Loading { * @param indexes Nonempty index selection; relative paths resolve against the asset root. * @return Owned data for upload(), prepared without a provider or backend calls. */ - [[nodiscard]] Prepared prepare_graphics(const std::vector& indexes) const; + [[nodiscard]] CE_API Prepared prepare_graphics(const std::vector& indexes) const; /** Consumes one prepared value without reopening its image files. Existing * keys remain; each successful new entry can become visible immediately. * Failure can leave any completed cache entries, including all entries if @@ -101,24 +103,24 @@ namespace CE::Assets::Loading { * @param provider Active resource backend, used on its loading owner/current context. * @param replace_shaders Whether existing shader programs/materials may be replaced. */ - void upload(Prepared prepared, CE::Rendering::Resources::Provider& provider, bool replace_shaders = false); + CE_API void upload(Prepared prepared, CE::Rendering::Resources::Provider& provider, bool replace_shaders = false); /** Prepare the complete root and synchronously upload it without shader replacement. * Preparation failures leave caches unchanged; upload failures retain the * partial-publication behavior of upload(). * @param provider Active backend, used on its loading owner/current context. */ - void load_assets(CE::Rendering::Resources::Provider& provider); + CE_API void load_assets(CE::Rendering::Resources::Provider& provider); /** Discover and register unmanaged paths for manual asset loading. * No JSON parsing, image decoding or provider is required. Filesystem and * allocation failures propagate; PNG images remain outside the untyped registry. */ - void register_files() const; + CE_API void register_files() const; /** Observe the last upload attempt, including partial publication. * The snapshot is synchronized, but provides no upload/rollback ordering * guarantee beyond the loading-owner contract. * @return Copied counters and completion status for the last attempt. */ - [[nodiscard]] UploadStats diagnostics() const; + [[nodiscard]] CE_API UploadStats diagnostics() const; /** @return Owned snapshot of the last successfully submitted asset definitions. * Readers retain it across later uploads and loader destruction. This is * metadata, not an atomic snapshot of the global caches. @@ -138,10 +140,14 @@ namespace CE::Assets::Loading { * @param root_path Root to initialize, or match against the existing singleton. * @return Borrowed process-lifetime loader with that root. */ - static Loader& get(const std::filesystem::path& root_path); + CE_API static Loader& get(const std::filesystem::path& root_path); /** Retrieve the configured legacy singleton without initializing it. * @return Borrowed process-lifetime loader; fails if initialization is unpublished. */ - static Loader& get(); + CE_API static Loader& get(); }; } + +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; +} diff --git a/projects/engine/include/cheryl/core/resources/asset-management/file-registry.h b/projects/engine/include/cheryl/core/resources/asset-management/file-registry.h index df28fa75..238512ce 100644 --- a/projects/engine/include/cheryl/core/resources/asset-management/file-registry.h +++ b/projects/engine/include/cheryl/core/resources/asset-management/file-registry.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -24,23 +26,27 @@ namespace CE::Assets::Loading { // Append files without replacing earlier registrations. Paths become // lexically normalized absolute paths; identical paths are deduplicated. // Allocation failure can leave completed insertions registered. - void register_files(const std::vector& files); + CE_API void register_files(const std::vector& files); // Append a prepared registry using the same path-deduplication policy. - void register_files(const FileRegistry& files); + CE_API void register_files(const FileRegistry& files); // Returns an owned, sorted snapshot for this exact basename. Later // registration cannot invalidate it; missing names return an empty vector. - [[nodiscard]] std::vector get_files_named(std::string_view name) const; + [[nodiscard]] CE_API std::vector get_files_named(std::string_view name) const; // Returns the lexically first matching absolute path, if any. - [[nodiscard]] std::optional get_file_named(std::string_view name) const; + [[nodiscard]] CE_API std::optional get_file_named(std::string_view name) const; // Resolve an exact registered path, not the first matching basename. // Relative input is interpreted against the current working directory. - [[nodiscard]] std::optional get_file_at(const std::filesystem::path& path) const; + [[nodiscard]] CE_API std::optional get_file_at(const std::filesystem::path& path) const; private: - void insert_file(const std::filesystem::path& path); + CE_API void insert_file(const std::filesystem::path& path); }; } + +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; +} diff --git a/projects/engine/include/cheryl/core/resources/asset-management/font-mgr.h b/projects/engine/include/cheryl/core/resources/asset-management/font-mgr.h index 4bc9f221..38aad06b 100644 --- a/projects/engine/include/cheryl/core/resources/asset-management/font-mgr.h +++ b/projects/engine/include/cheryl/core/resources/asset-management/font-mgr.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -18,8 +20,8 @@ namespace CE::Text::Legacy { * Loading uses STB's printable-ASCII atlas and collection face zero. * System-font discovery/default candidate selection are separate utilities. */ - struct Manager final : Store, - CE::State::Singleton { + struct CE_TYPE_API Manager final : Store, + CE::State::Singleton { Manager() = default; ~Manager() override = default; @@ -29,17 +31,21 @@ namespace CE::Text::Legacy { * publication becomes the default if none exists. Errors propagate without * rolling back earlier publications or trying an alternate system font. */ - void load(const std::vector& files, CE::Rendering::Resources::Provider& provider); + CE_API void load(const std::vector& files, CE::Rendering::Resources::Provider& provider); /** Retain the published default, or return nullptr before loading/after clear. */ - [[nodiscard]] Handle default_font() const; + [[nodiscard]] CE_API Handle default_font() const; /** Clear cache/default ownership; existing shared handles remain valid. * This does not release or replace the shared provider/loading-owner domain. */ - void clear_assets() noexcept override; + CE_API void clear_assets() noexcept override; private: std::filesystem::path default_font_path_; }; } + +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; +} diff --git a/projects/engine/include/cheryl/core/resources/asset-management/manifest-loader.h b/projects/engine/include/cheryl/core/resources/asset-management/manifest-loader.h index 0ee32ccf..69262871 100644 --- a/projects/engine/include/cheryl/core/resources/asset-management/manifest-loader.h +++ b/projects/engine/include/cheryl/core/resources/asset-management/manifest-loader.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -21,18 +23,18 @@ namespace CE::Assets::Manifests { * documents and image bounds are checked by Loader after it has collected the full asset set. */ struct Reader { - [[nodiscard]] static Graphics load(const std::filesystem::path& file); - [[nodiscard]] static Graphics parse(std::istream& input, const std::filesystem::path& source); - [[nodiscard]] static CE::Assets::Shaders::Manifest load_shader(const std::filesystem::path& file); - [[nodiscard]] static CE::Assets::Shaders::Manifest parse_shader(std::istream& input, const std::filesystem::path& source); + [[nodiscard]] CE_API static Graphics load(const std::filesystem::path& file); + [[nodiscard]] CE_API static Graphics parse(std::istream& input, const std::filesystem::path& source); + [[nodiscard]] CE_API static CE::Assets::Shaders::Manifest load_shader(const std::filesystem::path& file); + [[nodiscard]] CE_API static CE::Assets::Shaders::Manifest parse_shader(std::istream& input, const std::filesystem::path& source); // Open only registered graphics-manifests.json indexes and the graphics // documents they list. References are graphics-relative exact registered // paths; repeated references are parsed once across the entire batch. - [[nodiscard]] static std::vector load_graphics(const CE::Assets::Loading::FileRegistry& files); + [[nodiscard]] CE_API static std::vector load_graphics(const CE::Assets::Loading::FileRegistry& files); // Empty selection uses every registered index. Explicit indexes permit a // focused batch while references still resolve against the full registry. - [[nodiscard]] static Definitions + [[nodiscard]] CE_API static Definitions load_graphics_definitions(const CE::Assets::Loading::FileRegistry& files, const std::vector& indexes = {}); }; } diff --git a/projects/engine/include/cheryl/core/resources/asset-management/material-mgr.h b/projects/engine/include/cheryl/core/resources/asset-management/material-mgr.h index 9e809a53..1a4a2a75 100644 --- a/projects/engine/include/cheryl/core/resources/asset-management/material-mgr.h +++ b/projects/engine/include/cheryl/core/resources/asset-management/material-mgr.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -19,15 +21,19 @@ namespace CE::Assets::Materials { * Invoke synchronously on the active provider/loading owner, outside cache locks. * Builder captures must survive the call; its external side effects are not rolled back. */ - struct Manager final : CE::Assets::Cache::Store, - CE::State::Singleton { + struct CE_TYPE_API Manager final : CE::Assets::Cache::Store, + CE::State::Singleton { using Builder = std::function(CE::Rendering::Resources::Provider&)>; Manager() = default; ~Manager() override = default; // Exact path key; an existing entry skips the builder. - void load(const std::filesystem::path& key, CE::Rendering::Resources::Provider& provider, const Builder& builder); + CE_API void load(const std::filesystem::path& key, CE::Rendering::Resources::Provider& provider, const Builder& builder); // Replace or insert one generation; empty builders/null candidates throw. - void reload(const std::filesystem::path& key, CE::Rendering::Resources::Provider& provider, const Builder& builder); + CE_API void reload(const std::filesystem::path& key, CE::Rendering::Resources::Provider& provider, const Builder& builder); }; } + +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; +} diff --git a/projects/engine/include/cheryl/core/resources/asset-management/shader-asset-mgr.h b/projects/engine/include/cheryl/core/resources/asset-management/shader-asset-mgr.h index d72077c2..1e1ec392 100644 --- a/projects/engine/include/cheryl/core/resources/asset-management/shader-asset-mgr.h +++ b/projects/engine/include/cheryl/core/resources/asset-management/shader-asset-mgr.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -24,9 +26,9 @@ namespace CE::Assets::Shaders { /** Qualified identities share the existing provider domain. Metadata and the * resource it describes are published as one immutable generation. */ - struct Manager final : CE::Assets::Cache::Store, - CE::State::Singleton { - [[nodiscard]] std::shared_ptr find_program(const std::string& id) const; + struct CE_TYPE_API Manager final : CE::Assets::Cache::Store, + CE::State::Singleton { + [[nodiscard]] CE_API std::shared_ptr find_program(const std::string& id) const; /** Look up an already loaded material without compiling or uploading resources. * The retained handle survives cache replacement/clearing; native use * still obeys its provider lifetime. Lookup uses the cache lock and rejects @@ -34,10 +36,15 @@ namespace CE::Assets::Shaders { * @param id Exact qualified namespace:name identity. * @return Retained immutable material, or null for a missing identity. */ - [[nodiscard]] std::shared_ptr find_material(const std::string& id) const; + [[nodiscard]] CE_API std::shared_ptr find_material(const std::string& id) const; // Checks the complete batch before publication; unsupported capabilities // and existing identities of the wrong kind reject explicitly. - void validate(const std::vector& manifests, CE::Rendering::Resources::Provider& provider, bool replace = false) const; - void load(const std::vector& manifests, CE::Rendering::Resources::Provider& provider, bool replace = false); + CE_API void + validate(const std::vector& manifests, CE::Rendering::Resources::Provider& provider, bool replace = false) const; + CE_API void load(const std::vector& manifests, CE::Rendering::Resources::Provider& provider, bool replace = false); }; } + +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; +} diff --git a/projects/engine/include/cheryl/core/resources/asset-management/shader-mgr.h b/projects/engine/include/cheryl/core/resources/asset-management/shader-mgr.h index e67fdb84..35efa50e 100644 --- a/projects/engine/include/cheryl/core/resources/asset-management/shader-mgr.h +++ b/projects/engine/include/cheryl/core/resources/asset-management/shader-mgr.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -17,22 +19,26 @@ namespace CE::Assets::Shaders { * provider/loading owner; borrowed inputs are needed only through the call. * Camera/draw state is mutable program access on the backend owner. */ - struct ProgramCache final : ProgramStore, - CE::State::Singleton { + struct CE_TYPE_API ProgramCache final : ProgramStore, + CE::State::Singleton { ProgramCache() = default; ~ProgramCache() override = default; // Existing keys skip linking; get_asset() returns a retained program or null. - void load( + CE_API void load( const std::filesystem::path& key, const std::vector& stages, CE::Rendering::Resources::Provider& provider ); // Build before replacing/inserting one entry; a throw/null program preserves // the previous entry. Existing readers retain the previous program generation. - void reload( + CE_API void reload( const std::filesystem::path& key, const std::vector& stages, CE::Rendering::Resources::Provider& provider ); }; } + +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; +} diff --git a/projects/engine/include/cheryl/core/resources/asset-management/sprite-mgr.h b/projects/engine/include/cheryl/core/resources/asset-management/sprite-mgr.h index 668dcb92..a166dbcb 100644 --- a/projects/engine/include/cheryl/core/resources/asset-management/sprite-mgr.h +++ b/projects/engine/include/cheryl/core/resources/asset-management/sprite-mgr.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -19,8 +21,8 @@ namespace CE::Assets::Sprites { * existing IDs stay unchanged and a batch failure can leave earlier entries. * Inherited get_asset() returns a strong handle or null under the cache lock. */ - struct Manager final : Store, - CE::State::Singleton { + struct CE_TYPE_API Manager final : Store, + CE::State::Singleton { Manager() = default; ~Manager() override = default; using Store::get_asset; @@ -29,13 +31,13 @@ namespace CE::Assets::Sprites { * @param name Local asset name, copied into the lookup key. * @return Retained sprite handle, or null if absent; it survives cache clearing. */ - [[nodiscard]] Handle get_asset(std::string_view name_space, std::string_view name) const; + [[nodiscard]] CE_API Handle get_asset(std::string_view name_space, std::string_view name) const; /** Snapshot published sprites in one exact namespace without loading entries. * @param name_space Namespace matched against each retained definition. * @return Owned map keyed by local name in lexical order, empty if absent; * copied handles survive cache clears. */ - [[nodiscard]] std::map> get_assets(std::string_view name_space) const; + [[nodiscard]] CE_API std::map> get_assets(std::string_view name_space) const; /** Construct and publish sprites whose IDs are not already cached. * Missing textures/materials, invalid definitions or upload failures can * leave earlier sprites published; existing IDs retain their original handles. @@ -43,6 +45,10 @@ namespace CE::Assets::Sprites { * optional shader materials must already be loaded. * @param provider Active backend used on its loading owner/current context. */ - void load(const std::vector& definitions, CE::Rendering::Resources::Provider& provider); + CE_API void load(const std::vector& definitions, CE::Rendering::Resources::Provider& provider); }; } + +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; +} diff --git a/projects/engine/include/cheryl/core/resources/asset-management/texture-mgr.h b/projects/engine/include/cheryl/core/resources/asset-management/texture-mgr.h index a7040789..401e5424 100644 --- a/projects/engine/include/cheryl/core/resources/asset-management/texture-mgr.h +++ b/projects/engine/include/cheryl/core/resources/asset-management/texture-mgr.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -11,17 +13,21 @@ namespace CE::Rendering::Resources { namespace CE::Assets::Images { using Store = CE::Assets::Cache::Store; - struct Manager final : Store, - CE::State::Singleton { + struct CE_TYPE_API Manager final : Store, + CE::State::Singleton { Manager() = default; ~Manager() override = default; // Exact lexically normalized path first; a bare filename may uniquely match // another path. Missing returns null, ambiguity throws. contains() is exact-key only. - [[nodiscard]] Handle get_asset(const std::filesystem::path& file) const override; + [[nodiscard]] CE_API Handle get_asset(const std::filesystem::path& file) const override; // Serialize on the cache/provider loading owner. Existing normalized keys // are reused without rereading/replacing their immutable image contents. // Batch failure can leave earlier entries; input pixels are not retained. - void load(const std::vector& files, CE::Rendering::Resources::Provider& provider); - void upload(const std::filesystem::path& file, const Decoded& image, CE::Rendering::Resources::Provider& provider); + CE_API void load(const std::vector& files, CE::Rendering::Resources::Provider& provider); + CE_API void upload(const std::filesystem::path& file, const Decoded& image, CE::Rendering::Resources::Provider& provider); }; } + +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; +} diff --git a/projects/engine/include/cheryl/core/resources/asset-management/tileset-mgr.h b/projects/engine/include/cheryl/core/resources/asset-management/tileset-mgr.h index c25b8797..0ce13ea5 100644 --- a/projects/engine/include/cheryl/core/resources/asset-management/tileset-mgr.h +++ b/projects/engine/include/cheryl/core/resources/asset-management/tileset-mgr.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -16,10 +18,14 @@ namespace CE::Assets::Tilesets { * existing IDs stay unchanged and a batch failure can leave earlier entries. * Inherited get_asset() returns a strong handle or null under the cache lock. */ - struct Manager final : Store, - CE::State::Singleton { + struct CE_TYPE_API Manager final : Store, + CE::State::Singleton { Manager() = default; ~Manager() override = default; - void load(const std::vector& definitions, CE::Rendering::Resources::Provider& provider); + CE_API void load(const std::vector& definitions, CE::Rendering::Resources::Provider& provider); }; } + +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; +} diff --git a/projects/engine/include/cheryl/core/resources/fileio/file-mgr.h b/projects/engine/include/cheryl/core/resources/fileio/file-mgr.h index 9a7e7545..44307f48 100644 --- a/projects/engine/include/cheryl/core/resources/fileio/file-mgr.h +++ b/projects/engine/include/cheryl/core/resources/fileio/file-mgr.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -17,7 +19,7 @@ namespace CE::Files { class FileIndex { public: /** Scan root_path using search_directory(); missing/non-directory roots add nothing. */ - FileIndex(const std::filesystem::path& root_path); + CE_API FileIndex(const std::filesystem::path& root_path); /** Append regular files beneath an unvisited directory and mark encountered * directories as visited. Keys use path equality without canonicalization; @@ -26,7 +28,7 @@ namespace CE::Files { * Enumeration is unsorted. Filesystem/allocation errors propagate and can leave * a partial index with the root marked visited; recreate the index to retry. */ - void search_directory(const std::filesystem::path& directory); + CE_API void search_directory(const std::filesystem::path& directory); /** Borrow the manager-owned vector for a lowercased extension key. A missing * key inserts an empty bucket; use ".png", not "png", or "file_no_ext". @@ -35,7 +37,7 @@ namespace CE::Files { * contents and invalidate its element references/iterators. * Serialize this lookup with all other access; insertion can allocate/throw. */ - const std::vector<::CE::Files::Path>& get_files_of_type(::CE::Files::Extension extension); + CE_API const std::vector<::CE::Files::Path>& get_files_of_type(::CE::Files::Extension extension); // Borrow all extension buckets without inserting missing keys. Serialize // access with directory indexing; the manager owns the returned index. diff --git a/projects/engine/include/cheryl/core/resources/fileio/fonts-system.h b/projects/engine/include/cheryl/core/resources/fileio/fonts-system.h index 2aae1d46..5c7061a2 100644 --- a/projects/engine/include/cheryl/core/resources/fileio/fonts-system.h +++ b/projects/engine/include/cheryl/core/resources/fileio/fonts-system.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -8,10 +10,10 @@ namespace CE::Text { * HOME/XDG_DATA_HOME/WINDIR/LOCALAPPDATA values. No existence check or deduplication * occurs here. Keep environment mutation serialized with this call. */ - [[nodiscard]] std::vector system_font_directories(); + [[nodiscard]] CE_API std::vector system_font_directories(); /** Discover fonts using system_font_directories() with the explicit-root overload's policy. */ - [[nodiscard]] std::vector find_system_fonts(); + [[nodiscard]] CE_API std::vector find_system_fonts(); /** Best-effort recursive discovery of regular .ttf/.otf/.ttc/.otc paths, * matched case-insensitively. Missing/non-directory/inaccessible roots and @@ -21,7 +23,7 @@ namespace CE::Text { * aliases are not canonicalized and font contents are not validated. * Filesystem errors use error_code; allocation/path-conversion failures propagate. */ - [[nodiscard]] std::vector find_system_fonts(const std::vector& directories); + [[nodiscard]] CE_API std::vector find_system_fonts(const std::vector& directories); /** Choose a candidate by the filename preference order documented in * docs/assets/file-and-font-discovery.md. Filename matching ignores case; @@ -29,5 +31,5 @@ namespace CE::Text { * return the minimum path; an empty list returns nullopt. No I/O, font-family * inspection or load-failure fallback is performed. The path is an owned copy. */ - [[nodiscard]] std::optional select_default_system_font(const std::vector& fonts); + [[nodiscard]] CE_API std::optional select_default_system_font(const std::vector& fonts); } diff --git a/projects/engine/include/cheryl/core/subsystems/event-bus.h b/projects/engine/include/cheryl/core/subsystems/event-bus.h index e77f14f8..b14a2677 100644 --- a/projects/engine/include/cheryl/core/subsystems/event-bus.h +++ b/projects/engine/include/cheryl/core/subsystems/event-bus.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include "event-channel.h" @@ -99,8 +101,8 @@ namespace CE::Events { std::exception_ptr failure; std::atomic entered{false}; - explicit DeliveryTicket(std::shared_ptr value); - ~DeliveryTicket(); + CE_API explicit DeliveryTicket(std::shared_ptr value); + CE_API ~DeliveryTicket(); }; struct State { std::mutex mutex; @@ -135,14 +137,14 @@ namespace CE::Events { }; EventBus() = default; - ~EventBus(); + CE_API ~EventBus(); EventBus(const EventBus&) = delete; EventBus& operator=(const EventBus&) = delete; // A registration is intentionally persistent even when its ID is ignored. // Empty callback or queued delivery without errors throws invalid_args; // closed bus throws failed_operation. Strings/callables are owned after registration. - Registration register_listener( + CE_API Registration register_listener( const std::string& event, Callback callback, Delivery delivery = Delivery{}, @@ -150,7 +152,7 @@ namespace CE::Events { ); // Borrow payload for this call; listeners receive copies. Immediate exceptions // propagate, queued failures go to their error sink. A closed bus rejects dispatch. - void dispatch(const std::string& event, const std::any& payload); + CE_API void dispatch(const std::string& event, const std::any& payload); // Exact name/type channel; callback copies/registration and delivery obey // the same lifetime as named listeners. Empty callbacks reject as usual. template <::CE::Events::EventPayload Payload> @@ -180,30 +182,25 @@ namespace CE::Events { dispatch_channel(event.name(), typeid(Payload), owned); } // Invalidation prevents new invocation entry; already-running work finishes. - bool unregister_listener(const Registration& registration); + CE_API bool unregister_listener(const Registration& registration); // Only wait after invalidation. Waiting on one's own invocation rejects. - void wait_for_listener(const Registration& registration) const; - bool unregister_and_wait(const Registration& registration); + CE_API void wait_for_listener(const Registration& registration) const; + CE_API bool unregister_and_wait(const Registration& registration); // Every concurrent close returns after all invocation gates are closed. // Already-running callbacks retain ownership and are not waited for. - void close(); - [[nodiscard]] ::CE::Events::EventStats diagnostics() const; + CE_API void close(); + [[nodiscard]] CE_API ::CE::Events::EventStats diagnostics() const; // Explicit unlocked observer; registry edits/native callbacks/destructors // do not emit ordinary logs. Payloads and names are not sampled. - void report_diagnostics() const noexcept; + CE_API void report_diagnostics() const noexcept; private: - Registration register_channel( - const std::string& event, - std::type_index payload, - Callback callback, - Delivery delivery, - ErrorHandler errors - ); - void dispatch_channel(std::string_view event, std::type_index type, const std::any& payload); - static void invoke(const std::shared_ptr& listener, const std::any& payload); - static void invalidate(const std::shared_ptr& listener); - static void deliver(const std::shared_ptr& listener, const std::any& payload); - static void report_error(const std::shared_ptr& listener, std::exception_ptr failure) noexcept; + CE_API Registration + register_channel(const std::string& event, std::type_index payload, Callback callback, Delivery delivery, ErrorHandler errors); + CE_API void dispatch_channel(std::string_view event, std::type_index type, const std::any& payload); + CE_API static void invoke(const std::shared_ptr& listener, const std::any& payload); + CE_API static void invalidate(const std::shared_ptr& listener); + CE_API static void deliver(const std::shared_ptr& listener, const std::any& payload); + CE_API static void report_error(const std::shared_ptr& listener, std::exception_ptr failure) noexcept; }; } diff --git a/projects/engine/include/cheryl/core/subsystems/event-system.h b/projects/engine/include/cheryl/core/subsystems/event-system.h index 1aedbd31..b4e79808 100644 --- a/projects/engine/include/cheryl/core/subsystems/event-system.h +++ b/projects/engine/include/cheryl/core/subsystems/event-system.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "event-bus.h" #include @@ -20,13 +22,13 @@ namespace CE::Events { EventSystem() = default; [[nodiscard]] ::CE::Events::EventBus& bus() { return bus_; } - Registration register_listener( + CE_API Registration register_listener( const std::string& event, Callback callback, Delivery delivery = Delivery{}, ErrorHandler errors = ErrorHandler{} ); - void dispatch(const std::string& event, const std::any& payload); + CE_API void dispatch(const std::string& event, const std::any& payload); template <::CE::Events::EventPayload Payload> Registration register_listener( const ::CE::Events::EventChannel& event, @@ -46,3 +48,7 @@ namespace CE::Events { bool unregister_and_wait(const Registration& registration) { return bus_.unregister_and_wait(registration); } }; } + +namespace CE::State { + template <> CE_API auto Singleton::storage() -> Storage&; +} diff --git a/projects/engine/include/cheryl/export.h b/projects/engine/include/cheryl/export.h new file mode 100644 index 00000000..6f8d6dfe --- /dev/null +++ b/projects/engine/include/cheryl/export.h @@ -0,0 +1,108 @@ +#pragma once + +// Every binary owns its exports; a module imports Engine while exporting itself. +#if defined(_WIN32) +#define CE_DETAIL_EXPORT __declspec(dllexport) +#define CE_DETAIL_IMPORT __declspec(dllimport) +#elif defined(__GNUC__) || defined(__clang__) +#define CE_DETAIL_EXPORT __attribute__((visibility("default"))) +#define CE_DETAIL_IMPORT __attribute__((visibility("default"))) +#else +#define CE_DETAIL_EXPORT +#define CE_DETAIL_IMPORT +#endif + +// ELF RTTI needs public type visibility even when only selected methods export. +#if !defined(_WIN32) && (defined(__GNUC__) || defined(__clang__)) +#define CE_TYPE_API __attribute__((visibility("default"))) +#else +#define CE_TYPE_API +#endif + +#if defined(CE_ENGINE_STATIC) +#define CE_API +#elif defined(CE_BUILDING_ENGINE) +#define CE_API CE_DETAIL_EXPORT +#else +#define CE_API CE_DETAIL_IMPORT +#endif + +#if defined(CE_NATIVE_GLFW_STATIC) +#define CE_NATIVE_GLFW_API +#elif defined(CE_BUILDING_NATIVE_GLFW) +#define CE_NATIVE_GLFW_API CE_DETAIL_EXPORT +#else +#define CE_NATIVE_GLFW_API CE_DETAIL_IMPORT +#endif + +#if defined(CE_OPENGL_STATIC) +#define CE_OPENGL_API +#elif defined(CE_BUILDING_OPENGL) +#define CE_OPENGL_API CE_DETAIL_EXPORT +#else +#define CE_OPENGL_API CE_DETAIL_IMPORT +#endif + +#if defined(CE_UI_TGUI_STATIC) +#define CE_UI_TGUI_API +#elif defined(CE_BUILDING_UI_TGUI) +#define CE_UI_TGUI_API CE_DETAIL_EXPORT +#else +#define CE_UI_TGUI_API CE_DETAIL_IMPORT +#endif + +#if defined(CE_UI_RMLUI_STATIC) +#define CE_UI_RMLUI_API +#elif defined(CE_BUILDING_UI_RMLUI) +#define CE_UI_RMLUI_API CE_DETAIL_EXPORT +#else +#define CE_UI_RMLUI_API CE_DETAIL_IMPORT +#endif + +#if defined(CE_AUDIO_MINIAUDIO_STATIC) +#define CE_AUDIO_MINIAUDIO_API +#elif defined(CE_BUILDING_AUDIO_MINIAUDIO) +#define CE_AUDIO_MINIAUDIO_API CE_DETAIL_EXPORT +#else +#define CE_AUDIO_MINIAUDIO_API CE_DETAIL_IMPORT +#endif + +#if defined(CE_STARTUP_STATIC) +#define CE_STARTUP_API +#elif defined(CE_BUILDING_STARTUP) +#define CE_STARTUP_API CE_DETAIL_EXPORT +#else +#define CE_STARTUP_API CE_DETAIL_IMPORT +#endif + +#if defined(CE_OPENGL_STARTUP_STATIC) +#define CE_OPENGL_STARTUP_API +#elif defined(CE_BUILDING_OPENGL_STARTUP) +#define CE_OPENGL_STARTUP_API CE_DETAIL_EXPORT +#else +#define CE_OPENGL_STARTUP_API CE_DETAIL_IMPORT +#endif + +#if defined(CE_DEBUG_TERMINAL_STATIC) +#define CE_DEBUG_TERMINAL_API +#elif defined(CE_BUILDING_DEBUG_TERMINAL) +#define CE_DEBUG_TERMINAL_API CE_DETAIL_EXPORT +#else +#define CE_DEBUG_TERMINAL_API CE_DETAIL_IMPORT +#endif + +#if defined(CE_WORKER_TOPOLOGY_HWLOC_STATIC) +#define CE_WORKER_TOPOLOGY_HWLOC_API +#elif defined(CE_BUILDING_WORKER_TOPOLOGY_HWLOC) +#define CE_WORKER_TOPOLOGY_HWLOC_API CE_DETAIL_EXPORT +#else +#define CE_WORKER_TOPOLOGY_HWLOC_API CE_DETAIL_IMPORT +#endif + +#if defined(CE_WORKER_TOPOLOGY_JSON_STATIC) +#define CE_WORKER_TOPOLOGY_JSON_API +#elif defined(CE_BUILDING_WORKER_TOPOLOGY_JSON) +#define CE_WORKER_TOPOLOGY_JSON_API CE_DETAIL_EXPORT +#else +#define CE_WORKER_TOPOLOGY_JSON_API CE_DETAIL_IMPORT +#endif diff --git a/projects/engine/include/cheryl/math/anchor.h b/projects/engine/include/cheryl/math/anchor.h index 6d9dcb0a..e55617c6 100644 --- a/projects/engine/include/cheryl/math/anchor.h +++ b/projects/engine/include/cheryl/math/anchor.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -19,8 +21,8 @@ namespace CE { // Case-sensitive two-letter names (CL/ML and CR/MR are aliases); unknown // names or enum values fall back to Center rather than throwing. - [[nodiscard]] AnchorType get_anchor(const std::string& anchor); - [[nodiscard]] Pivot get_pivot(AnchorType anchor); + [[nodiscard]] CE_API AnchorType get_anchor(const std::string& anchor); + [[nodiscard]] CE_API Pivot get_pivot(AnchorType anchor); /** Convert a top-left-origin image rectangle into local Y-up geometry. * The pointer overloads and MakeQuad write six vertices for two independent triangles; @@ -31,7 +33,7 @@ namespace CE { * they are borrowed through the call. Invalid pivot/texture throws before writes. */ struct Anchor { - [[nodiscard]] static ::CE::Rendering::Geometry::Quad MakeQuad( + [[nodiscard]] CE_API static ::CE::Rendering::Geometry::Quad MakeQuad( ::CE::Rendering::Geometry::Pivot pivot, std::uint32_t texture_width, std::uint32_t texture_height, @@ -40,7 +42,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - [[nodiscard]] static ::CE::Rendering::Geometry::QuadTriangleStrip MakeQuadStrip( + [[nodiscard]] CE_API static ::CE::Rendering::Geometry::QuadTriangleStrip MakeQuadStrip( ::CE::Rendering::Geometry::Pivot pivot, std::uint32_t texture_width, std::uint32_t texture_height, @@ -49,7 +51,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - static void MakePivot( + CE_API static void MakePivot( ::CE::Rendering::Geometry::Pivot pivot, float* vertices, std::uint32_t texture_width, @@ -59,7 +61,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - static void MakePivot( + CE_API static void MakePivot( ::CE::Rendering::Geometry::Pivot pivot, ::CE::Rendering::Geometry::Vertex2D* vertices, std::uint32_t texture_width, @@ -70,7 +72,7 @@ namespace CE { std::uint32_t y0 = 0 ); - static void MakeAnchor( + CE_API static void MakeAnchor( ::CE::Rendering::Geometry::AnchorType type, float* vertices, std::uint32_t texture_width, @@ -80,7 +82,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - static void MakeAnchor( + CE_API static void MakeAnchor( ::CE::Rendering::Geometry::AnchorType type, ::CE::Rendering::Geometry::Vertex2D* vertices, std::uint32_t texture_width, @@ -91,7 +93,7 @@ namespace CE { std::uint32_t y0 = 0 ); - static void Center( + CE_API static void Center( ::CE::Rendering::Geometry::Vertex2D* vertices, std::uint32_t texture_width, std::uint32_t texture_height, @@ -100,7 +102,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - static void TopLeft( + CE_API static void TopLeft( ::CE::Rendering::Geometry::Vertex2D* vertices, std::uint32_t texture_width, std::uint32_t texture_height, @@ -109,7 +111,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - static void TopRight( + CE_API static void TopRight( ::CE::Rendering::Geometry::Vertex2D* vertices, std::uint32_t texture_width, std::uint32_t texture_height, @@ -118,7 +120,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - static void BottomLeft( + CE_API static void BottomLeft( ::CE::Rendering::Geometry::Vertex2D* vertices, std::uint32_t texture_width, std::uint32_t texture_height, @@ -127,7 +129,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - static void BottomRight( + CE_API static void BottomRight( ::CE::Rendering::Geometry::Vertex2D* vertices, std::uint32_t texture_width, std::uint32_t texture_height, @@ -137,7 +139,7 @@ namespace CE { std::uint32_t y0 = 0 ); - static void Center( + CE_API static void Center( float* vertices, std::uint32_t texture_width, std::uint32_t texture_height, @@ -146,7 +148,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - static void TopLeft( + CE_API static void TopLeft( float* vertices, std::uint32_t texture_width, std::uint32_t texture_height, @@ -155,7 +157,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - static void TopRight( + CE_API static void TopRight( float* vertices, std::uint32_t texture_width, std::uint32_t texture_height, @@ -164,7 +166,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - static void BottomLeft( + CE_API static void BottomLeft( float* vertices, std::uint32_t texture_width, std::uint32_t texture_height, @@ -173,7 +175,7 @@ namespace CE { std::uint32_t x0 = 0, std::uint32_t y0 = 0 ); - static void BottomRight( + CE_API static void BottomRight( float* vertices, std::uint32_t texture_width, std::uint32_t texture_height, diff --git a/projects/engine/include/cheryl/templates/asset-mgr.h b/projects/engine/include/cheryl/templates/asset-mgr.h index 14f5e294..8c4fb9a0 100644 --- a/projects/engine/include/cheryl/templates/asset-mgr.h +++ b/projects/engine/include/cheryl/templates/asset-mgr.h @@ -1,4 +1,6 @@ #pragma once + +#include #include #include #include @@ -40,10 +42,10 @@ namespace CE::Assets::Cache { */ class Context { friend struct CE::Rendering::Resources::Provider; - inline static std::mutex provider_mutex_; - inline static const CE::Rendering::Resources::Provider* bound_provider_ = nullptr; - inline static std::thread::id owner_; - inline static bool releasing_ = false; + static CE_API std::mutex provider_mutex_; + static CE_API const CE::Rendering::Resources::Provider* bound_provider_; + static CE_API std::thread::id owner_; + static CE_API bool releasing_; public: static void verify_provider(const CE::Rendering::Resources::Provider& provider) { diff --git a/projects/engine/include/cheryl/text/font-collection.h b/projects/engine/include/cheryl/text/font-collection.h index a929b9da..ec6a7530 100644 --- a/projects/engine/include/cheryl/text/font-collection.h +++ b/projects/engine/include/cheryl/text/font-collection.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -60,7 +62,7 @@ namespace CE::Text { */ class FontCollection { std::shared_ptr data_; - explicit FontCollection(std::shared_ptr data); + CE_API explicit FontCollection(std::shared_ptr data); friend struct Detail::FontAccess; public: @@ -71,18 +73,18 @@ namespace CE::Text { * borrowed only for this call. * @return Owned ordered faces ending in the embedded fallback, shared by copies. */ - [[nodiscard]] static FontCollection load(const ::CE::Text::FontSelection& selection = {}); + [[nodiscard]] CE_API static FontCollection load(const ::CE::Text::FontSelection& selection = {}); /** @return Borrowed ordered face metadata, empty after moving from the collection. * Keep the retained collection data alive while using the view; this call * performs no discovery or loading. */ - [[nodiscard]] std::span faces() const noexcept; + [[nodiscard]] CE_API std::span faces() const noexcept; /** Test character-map coverage across the retained faces without a provider. * This does not select a font for a whole grapheme or shape text. A moved-from * collection or failure opening a retained face can reject a valid-scalar query. * @param scalar Unicode scalar value; surrogates/out-of-range values return false. * @return Whether at least one face maps the scalar to a glyph. */ - [[nodiscard]] bool covers(char32_t scalar) const; + [[nodiscard]] CE_API bool covers(char32_t scalar) const; }; } diff --git a/projects/engine/include/cheryl/text/layout.h b/projects/engine/include/cheryl/text/layout.h index c234998f..b53faa16 100644 --- a/projects/engine/include/cheryl/text/layout.h +++ b/projects/engine/include/cheryl/text/layout.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -58,8 +60,8 @@ namespace CE::Text { std::vector<::CE::Text::TextLine> lines_; float line_height_{}; - ShapedText(::CE::Text::FontCollection fonts, ::CE::Text::LayoutOptions options); - friend ShapedText layout_text(const FontCollection&, std::string_view, const LayoutOptions&); + CE_API ShapedText(::CE::Text::FontCollection fonts, ::CE::Text::LayoutOptions options); + friend CE_API ShapedText layout_text(const FontCollection&, std::string_view, const LayoutOptions&); public: [[nodiscard]] const ::CE::Text::FontCollection& fonts() const noexcept { return fonts_; } @@ -83,5 +85,5 @@ namespace CE::Text { * @param options Font size, paragraph direction, optional width and language hint. * @return Owned immutable layout, with scalar/byte mappings and retained font bytes. */ - [[nodiscard]] ShapedText layout_text(const FontCollection& fonts, std::string_view text, const LayoutOptions& options = {}); + [[nodiscard]] CE_API ShapedText layout_text(const FontCollection& fonts, std::string_view text, const LayoutOptions& options = {}); } diff --git a/projects/engine/include/cheryl/text/utf8.h b/projects/engine/include/cheryl/text/utf8.h index 367fa9dc..b5f0bbd1 100644 --- a/projects/engine/include/cheryl/text/utf8.h +++ b/projects/engine/include/cheryl/text/utf8.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -22,9 +24,9 @@ namespace CE::Text { // U+FFFD for one maximal subpart; byte_count always advances within the view. // Truncated prefixes are errors at this view's end; this is not a streaming API. // The caller keeps input stable for the call and advances by returned byte_count. - [[nodiscard]] std::optional decode_utf8_scalar(std::string_view text, std::size_t offset) noexcept; + [[nodiscard]] CE_API std::optional decode_utf8_scalar(std::string_view text, std::size_t offset) noexcept; // Decode the complete view into owned records, including embedded NUL and BOM. // No normalization, segmentation or source retention. Allocation failure propagates. - [[nodiscard]] std::vector decode_utf8(std::string_view text); + [[nodiscard]] CE_API std::vector decode_utf8(std::string_view text); } diff --git a/projects/engine/include/internals.h b/projects/engine/include/internals.h index 5746151d..6ae6125d 100644 --- a/projects/engine/include/internals.h +++ b/projects/engine/include/internals.h @@ -7,5 +7,5 @@ #include "internals/pointers.h" #include "internals/posh.h" #include -extern backward::SignalHandling sh; -extern backward::TraceResolver tr; +extern CE_API backward::SignalHandling sh; +extern CE_API backward::TraceResolver tr; diff --git a/projects/engine/include/internals/exceptions.h b/projects/engine/include/internals/exceptions.h index 30068717..684a8609 100644 --- a/projects/engine/include/internals/exceptions.h +++ b/projects/engine/include/internals/exceptions.h @@ -1,4 +1,6 @@ #pragma once + +#include #include "macros/debug.h" #include #include @@ -16,7 +18,7 @@ namespace CE::Exceptions { * The fallback changes the exception layout; consumers must rebuild together. * Allocation in caller argument expressions remains the caller's responsibility. */ - class exception_base : public std::exception { + class CE_TYPE_API exception_base : public std::exception { std::string msg; std::array fallback_{}; @@ -47,6 +49,7 @@ namespace CE::Exceptions { } } exception_base(exception_base&& other) noexcept = default; + CE_API ~exception_base() override; [[nodiscard]] const char* what() const noexcept override { return msg.empty() ? fallback_.data() : msg.c_str(); } // Emergency output prefers cause/location over the allocating trace prefix. [[nodiscard]] const char* diagnostic_summary() const noexcept { return fallback_[0] ? fallback_.data() : what(); } @@ -74,37 +77,37 @@ namespace CE::Exceptions { } }; - class invalid_args : public exception_base { + class CE_TYPE_API invalid_args : public exception_base { public: - invalid_args(const char* location_, uint32_t line_) noexcept; - invalid_args(const char* location_, uint32_t line_, const char* info_) noexcept; + CE_API invalid_args(const char* location_, uint32_t line_) noexcept; + CE_API invalid_args(const char* location_, uint32_t line_, const char* info_) noexcept; invalid_args(const char* location_, uint32_t line_, const std::string& info_) noexcept : invalid_args(location_, line_, info_.c_str()) {} }; - class runtime_exception : public exception_base { + class CE_TYPE_API runtime_exception : public exception_base { public: - runtime_exception(const char* location_, uint32_t line_, const char* info_) noexcept; + CE_API runtime_exception(const char* location_, uint32_t line_, const char* info_) noexcept; runtime_exception(const char* location_, uint32_t line_, const std::string& info_) noexcept : runtime_exception(location_, line_, info_.c_str()) {} - runtime_exception(const char* sub_type, const char* location_, uint32_t line_, const char* info_) noexcept; + CE_API runtime_exception(const char* sub_type, const char* location_, uint32_t line_, const char* info_) noexcept; }; - class bad_alloc : public runtime_exception { + class CE_TYPE_API bad_alloc : public runtime_exception { public: - bad_alloc(const char* location_, uint32_t line_) noexcept; + CE_API bad_alloc(const char* location_, uint32_t line_) noexcept; }; - class bad_request : public runtime_exception { + class CE_TYPE_API bad_request : public runtime_exception { public: - bad_request(const char* location_, uint32_t line_, const char* info_) noexcept; + CE_API bad_request(const char* location_, uint32_t line_, const char* info_) noexcept; bad_request(const char* location_, uint32_t line_, const std::string& info_) noexcept : bad_request(location_, line_, info_.c_str()) {} }; - class failed_operation : public runtime_exception { + class CE_TYPE_API failed_operation : public runtime_exception { public: - failed_operation(const char* location_, uint32_t line_, const char* info_) noexcept; + CE_API failed_operation(const char* location_, uint32_t line_, const char* info_) noexcept; failed_operation(const char* location_, uint32_t line_, const std::string& info_) noexcept : failed_operation(location_, line_, info_.c_str()) {} }; diff --git a/projects/engine/src/assets/resources/pipeline.cpp b/projects/engine/src/assets/resources/pipeline.cpp index 5539d6a5..7120d6bb 100644 --- a/projects/engine/src/assets/resources/pipeline.cpp +++ b/projects/engine/src/assets/resources/pipeline.cpp @@ -85,3 +85,5 @@ namespace CE::Rendering::Resources { return result; } } + +CE::Rendering::Resources::Pipeline::~Pipeline() = default; diff --git a/projects/engine/src/assets/resources/resource-provider.cpp b/projects/engine/src/assets/resources/resource-provider.cpp index cbf56dc1..952abef0 100644 --- a/projects/engine/src/assets/resources/resource-provider.cpp +++ b/projects/engine/src/assets/resources/resource-provider.cpp @@ -58,3 +58,10 @@ namespace CE::Rendering::Resources { CE::Assets::Cache::Context::release_provider(*this); } } + +namespace CE::Assets::Cache { + std::mutex Context::provider_mutex_; + const CE::Rendering::Resources::Provider* Context::bound_provider_ = nullptr; + std::thread::id Context::owner_; + bool Context::releasing_ = false; +} diff --git a/projects/engine/src/assets/types/2d/ffont.cpp b/projects/engine/src/assets/types/2d/ffont.cpp index b7de600a..1064b759 100644 --- a/projects/engine/src/assets/types/2d/ffont.cpp +++ b/projects/engine/src/assets/types/2d/ffont.cpp @@ -75,3 +75,22 @@ namespace CE::Text::Legacy { return {widths, std::move(geometry), std::move(texture)}; } } + +#if defined(_MSC_VER) +#pragma warning(push) +#pragma warning(disable : 4996) +#elif defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } +} +#if defined(_MSC_VER) +#pragma warning(pop) +#elif defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif diff --git a/projects/engine/src/assets/types/2d/font-stb-allocation-internal.h b/projects/engine/src/assets/types/2d/font-stb-allocation-internal.h index 5c8ea067..5e8255ec 100644 --- a/projects/engine/src/assets/types/2d/font-stb-allocation-internal.h +++ b/projects/engine/src/assets/types/2d/font-stb-allocation-internal.h @@ -1,12 +1,14 @@ #pragma once +#include + #include #include namespace CE::Text::Legacy::Detail { // Private allocation boundary for real stb rasterization. The public loader // uses new_delete_resource; acceptance tests can reject individual requests. - [[nodiscard]] AsciiFontData load_font_with_resource( + [[nodiscard]] CE_API AsciiFontData load_font_with_resource( const std::filesystem::path& path, int font_size, CE::Rendering::Resources::Provider& provider, diff --git a/projects/engine/src/assets/types/2d/font-upload-internal.h b/projects/engine/src/assets/types/2d/font-upload-internal.h index 62c2de9d..4ace4bae 100644 --- a/projects/engine/src/assets/types/2d/font-upload-internal.h +++ b/projects/engine/src/assets/types/2d/font-upload-internal.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -10,7 +12,7 @@ namespace CE::Text::Legacy::Detail { // Private production upload boundary: baking supplies owned glyph vertices // and a borrowed alpha atlas, both copied by the provider before return. - [[nodiscard]] AsciiFontData upload_baked_font( + [[nodiscard]] CE_API AsciiFontData upload_baked_font( CE::Rendering::Resources::Provider& provider, std::shared_ptr vertices, std::span alpha, diff --git a/projects/engine/src/assets/types/2d/stbfont.cpp b/projects/engine/src/assets/types/2d/stbfont.cpp index a588aa59..a0bde58c 100644 --- a/projects/engine/src/assets/types/2d/stbfont.cpp +++ b/projects/engine/src/assets/types/2d/stbfont.cpp @@ -227,3 +227,5 @@ namespace CE::Text::Legacy { ); } } + +CE::Text::Legacy::Font::~Font() = default; diff --git a/projects/engine/src/core/controls/input-interface.cpp b/projects/engine/src/core/controls/input-interface.cpp index a7e8018d..0d0a95a2 100644 --- a/projects/engine/src/core/controls/input-interface.cpp +++ b/projects/engine/src/core/controls/input-interface.cpp @@ -49,3 +49,5 @@ namespace CE::Input { published_poll_.store({}, std::memory_order_release); } } + +CE::Input::System::~System() = default; diff --git a/projects/engine/src/core/engine/engine-context-internal.h b/projects/engine/src/core/engine/engine-context-internal.h index 36b8c95f..ecd9f4f4 100644 --- a/projects/engine/src/core/engine/engine-context-internal.h +++ b/projects/engine/src/core/engine/engine-context-internal.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -10,7 +12,7 @@ namespace CE::Runtime::Detail { // Per-context fault boundary for its lazy owned root. The normal path keeps // an empty factory and constructs CE::Workers::Pool directly; no global override. struct ContextAccess { - static void + CE_API static void set_owned_worker_factory(CE::Runtime::Context& context, std::function(std::size_t)> factory); }; } diff --git a/projects/engine/src/core/engine/event-delivery-internal.h b/projects/engine/src/core/engine/event-delivery-internal.h index f6a3a8a0..b6575b75 100644 --- a/projects/engine/src/core/engine/event-delivery-internal.h +++ b/projects/engine/src/core/engine/event-delivery-internal.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -10,5 +12,5 @@ namespace CE::Events::Detail { // or throw without retaining it; it must never invoke it inline or wait for execution. using WorkerSubmission = std::function; - [[nodiscard]] CE::Events::EventBus::Delivery worker_stream_delivery(CE::Workers::Group group, WorkerSubmission submit); + [[nodiscard]] CE_API CE::Events::EventBus::Delivery worker_stream_delivery(CE::Workers::Group group, WorkerSubmission submit); } diff --git a/projects/engine/src/core/engine/worker-affinity.h b/projects/engine/src/core/engine/worker-affinity.h index 4697e3b2..e86acb0c 100644 --- a/projects/engine/src/core/engine/worker-affinity.h +++ b/projects/engine/src/core/engine/worker-affinity.h @@ -1,11 +1,13 @@ #pragma once +#include + #include "worker-pool-internal.h" #include namespace CE::Workers::Detail { - NativeAdapter native_worker_adapter(); - CE::Workers::Capabilities discover_capabilities(const NativeAdapter& adapter); - void apply_affinity(const NativeAdapter& adapter, const std::vector& cpus); + CE_API NativeAdapter native_worker_adapter(); + CE_API CE::Workers::Capabilities discover_capabilities(const NativeAdapter& adapter); + CE_API void apply_affinity(const NativeAdapter& adapter, const std::vector& cpus); } diff --git a/projects/engine/src/core/engine/worker-pool-internal.h b/projects/engine/src/core/engine/worker-pool-internal.h index 8d1a1824..4ecad2f8 100644 --- a/projects/engine/src/core/engine/worker-pool-internal.h +++ b/projects/engine/src/core/engine/worker-pool-internal.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -20,7 +22,7 @@ namespace CE::Workers::Detail { }; struct PoolAccess { - [[nodiscard]] static std::unique_ptr create(std::size_t worker_count, NativeAdapter adapter); - [[nodiscard]] static std::unique_ptr create(CE::Workers::PoolOptions options, NativeAdapter adapter); + [[nodiscard]] CE_API static std::unique_ptr create(std::size_t worker_count, NativeAdapter adapter); + [[nodiscard]] CE_API static std::unique_ptr create(CE::Workers::PoolOptions options, NativeAdapter adapter); }; } diff --git a/projects/engine/src/core/engine/worker-topology-internal.h b/projects/engine/src/core/engine/worker-topology-internal.h index 316f9228..8e578e07 100644 --- a/projects/engine/src/core/engine/worker-topology-internal.h +++ b/projects/engine/src/core/engine/worker-topology-internal.h @@ -1,9 +1,11 @@ #pragma once +#include + #include namespace CE::Workers::Detail { // Validate and intersect a provider snapshot with the pool creator's mask. - [[nodiscard]] CE::Workers::Topology prepare_topology(const CE::Workers::TopologyProvider& provider); - [[nodiscard]] bool complete_core_topology(const CE::Workers::Topology& topology); + [[nodiscard]] CE_API CE::Workers::Topology prepare_topology(const CE::Workers::TopologyProvider& provider); + [[nodiscard]] CE_API bool complete_core_topology(const CE::Workers::Topology& topology); } diff --git a/projects/engine/src/core/game-framework/game-runtime-internal.h b/projects/engine/src/core/game-framework/game-runtime-internal.h index 0999f10c..e2ce6263 100644 --- a/projects/engine/src/core/game-framework/game-runtime-internal.h +++ b/projects/engine/src/core/game-framework/game-runtime-internal.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -9,7 +11,7 @@ namespace CE::Runtime::Detail { struct GameRuntimeAccess { // Configure before run. Return an owned joinable thread, or throw // without retaining the callable. Production directly uses std::thread. - static void + CE_API static void set_simulation_thread_factory(::CE::Runtime::GameRuntime& runtime, std::function)> factory); }; } diff --git a/projects/engine/src/core/logging/log.cpp b/projects/engine/src/core/logging/log.cpp index a75b8026..93d95f50 100644 --- a/projects/engine/src/core/logging/log.cpp +++ b/projects/engine/src/core/logging/log.cpp @@ -4,6 +4,7 @@ #include namespace CE::Logging::Detail { + std::mutex default_logger_mutex; uint16_t next_log_id() noexcept { // This legacy diagnostic serial wraps; it is not a session/domain identity. // It publishes no other logger state, so relaxed ordering is sufficient. @@ -23,3 +24,34 @@ namespace CE::Diagnostics { } } } + +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } + template <> auto Singleton>::storage() -> Storage& { + static Storage value; + return value; + } + template <> auto Singleton>::storage() -> Storage& { + static Storage value; + return value; + } + template <> auto Singleton>::storage() -> Storage& { + static Storage value; + return value; + } + template <> auto Singleton>::storage() -> Storage& { + static Storage value; + return value; + } + template <> auto Singleton>::storage() -> Storage& { + static Storage value; + return value; + } + template <> auto Singleton>::storage() -> Storage& { + static Storage value; + return value; + } +} diff --git a/projects/engine/src/core/rendering/camera.cpp b/projects/engine/src/core/rendering/camera.cpp index 39b36471..18e4b4ed 100644 --- a/projects/engine/src/core/rendering/camera.cpp +++ b/projects/engine/src/core/rendering/camera.cpp @@ -7,6 +7,8 @@ #include namespace CE::Rendering { + CameraBase::~CameraBase() = default; + void CameraBase::set_framebuffer_size(const ::CE::Platform::FramebufferSize size) { if (size.width < 0 || size.height < 0) { throw ::CE::Exceptions::invalid_args(CE_HERE, "Framebuffer dimensions must not be negative"); diff --git a/projects/engine/src/core/resources/asset-management/asset-loader.cpp b/projects/engine/src/core/resources/asset-management/asset-loader.cpp index 3f8dbbcb..4501307f 100644 --- a/projects/engine/src/core/resources/asset-management/asset-loader.cpp +++ b/projects/engine/src/core/resources/asset-management/asset-loader.cpp @@ -338,3 +338,10 @@ namespace CE::Assets::Loading { } } } + +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } +} diff --git a/projects/engine/src/core/resources/asset-management/file-registry.cpp b/projects/engine/src/core/resources/asset-management/file-registry.cpp index b061bc85..085bf1e5 100644 --- a/projects/engine/src/core/resources/asset-management/file-registry.cpp +++ b/projects/engine/src/core/resources/asset-management/file-registry.cpp @@ -58,3 +58,10 @@ namespace CE::Assets::Loading { return *position; } } + +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } +} diff --git a/projects/engine/src/core/resources/asset-management/font-mgr.cpp b/projects/engine/src/core/resources/asset-management/font-mgr.cpp index eb687c26..2e422687 100644 --- a/projects/engine/src/core/resources/asset-management/font-mgr.cpp +++ b/projects/engine/src/core/resources/asset-management/font-mgr.cpp @@ -41,3 +41,10 @@ namespace CE::Text::Legacy { } } } + +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } +} diff --git a/projects/engine/src/core/resources/asset-management/material-mgr.cpp b/projects/engine/src/core/resources/asset-management/material-mgr.cpp index 12f0b1f9..6c05aea0 100644 --- a/projects/engine/src/core/resources/asset-management/material-mgr.cpp +++ b/projects/engine/src/core/resources/asset-management/material-mgr.cpp @@ -40,3 +40,10 @@ namespace CE::Assets::Materials { ); } } + +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } +} diff --git a/projects/engine/src/core/resources/asset-management/shader-asset-mgr.cpp b/projects/engine/src/core/resources/asset-management/shader-asset-mgr.cpp index cbb86613..453ead8a 100644 --- a/projects/engine/src/core/resources/asset-management/shader-asset-mgr.cpp +++ b/projects/engine/src/core/resources/asset-management/shader-asset-mgr.cpp @@ -101,3 +101,10 @@ namespace CE::Assets::Shaders { } } } + +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } +} diff --git a/projects/engine/src/core/resources/asset-management/shader-mgr.cpp b/projects/engine/src/core/resources/asset-management/shader-mgr.cpp index 2c824ba6..54f11b8d 100644 --- a/projects/engine/src/core/resources/asset-management/shader-mgr.cpp +++ b/projects/engine/src/core/resources/asset-management/shader-mgr.cpp @@ -43,3 +43,10 @@ namespace CE::Assets::Shaders { ); } } + +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } +} diff --git a/projects/engine/src/core/resources/asset-management/sprite-mgr.cpp b/projects/engine/src/core/resources/asset-management/sprite-mgr.cpp index 7fb40351..a4c8e0e1 100644 --- a/projects/engine/src/core/resources/asset-management/sprite-mgr.cpp +++ b/projects/engine/src/core/resources/asset-management/sprite-mgr.cpp @@ -72,3 +72,10 @@ namespace CE::Assets::Sprites { } } } + +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } +} diff --git a/projects/engine/src/core/resources/asset-management/texture-mgr.cpp b/projects/engine/src/core/resources/asset-management/texture-mgr.cpp index 4500dcd2..59a839e7 100644 --- a/projects/engine/src/core/resources/asset-management/texture-mgr.cpp +++ b/projects/engine/src/core/resources/asset-management/texture-mgr.cpp @@ -48,3 +48,10 @@ namespace CE::Assets::Images { } } } + +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } +} diff --git a/projects/engine/src/core/resources/asset-management/tileset-mgr.cpp b/projects/engine/src/core/resources/asset-management/tileset-mgr.cpp index 41df1841..9a509cab 100644 --- a/projects/engine/src/core/resources/asset-management/tileset-mgr.cpp +++ b/projects/engine/src/core/resources/asset-management/tileset-mgr.cpp @@ -57,3 +57,10 @@ namespace CE::Assets::Tilesets { } } } + +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } +} diff --git a/projects/engine/src/core/subsystems/event-system.cpp b/projects/engine/src/core/subsystems/event-system.cpp index a095afca..51b44297 100644 --- a/projects/engine/src/core/subsystems/event-system.cpp +++ b/projects/engine/src/core/subsystems/event-system.cpp @@ -12,3 +12,10 @@ namespace CE::Events { return bus_.register_listener(event, std::move(callback), std::move(delivery), std::move(errors)); } } + +namespace CE::State { + template <> auto Singleton::storage() -> Storage& { + static Storage value; + return value; + } +} diff --git a/projects/engine/src/internal/exceptions.cpp b/projects/engine/src/internal/exceptions.cpp index cf84e5d1..38888842 100644 --- a/projects/engine/src/internal/exceptions.cpp +++ b/projects/engine/src/internal/exceptions.cpp @@ -53,3 +53,5 @@ ::CE::Exceptions::bad_request::bad_request(const char* location_, uint32_t line_ ::CE::Exceptions::failed_operation::failed_operation(const char* location_, uint32_t line_, const char* info_) noexcept : ::CE::Exceptions::runtime_exception("failed_operation", location_, line_, info_) {} + +CE::Exceptions::exception_base::~exception_base() = default; diff --git a/projects/engine/src/text/font-collection.cpp b/projects/engine/src/text/font-collection.cpp index 6baf2757..0718cf9d 100644 --- a/projects/engine/src/text/font-collection.cpp +++ b/projects/engine/src/text/font-collection.cpp @@ -36,9 +36,14 @@ namespace CE::Text { } FontBytes read_font(const std::filesystem::path& path) { + std::error_code error; + if (!std::filesystem::is_regular_file(path, error)) { + if (!std::filesystem::exists(path, error) && !error) + return {}; + throw ::CE::Exceptions::runtime_exception(CE_HERE, "Font source is not a regular file '" + path.string() + "'"); + } std::ifstream input(path, std::ios::binary | std::ios::ate); if (!input) { - std::error_code error; if (!std::filesystem::exists(path, error) && !error) return {}; throw ::CE::Exceptions::runtime_exception(CE_HERE, "Unable to open font '" + path.string() + "'"); diff --git a/projects/engine/support/debug-terminal/CMakeLists.txt b/projects/engine/support/debug-terminal/CMakeLists.txt index 4f478b35..00c55224 100644 --- a/projects/engine/support/debug-terminal/CMakeLists.txt +++ b/projects/engine/support/debug-terminal/CMakeLists.txt @@ -2,7 +2,7 @@ if(TARGET Cheryl::DebugTerminal::Startup) return() endif() -add_library(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} STATIC EXCLUDE_FROM_ALL src/startup.cpp src/arguments.cpp) +add_library(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} ${CHERYL_LIBRARY_TYPE} EXCLUDE_FROM_ALL src/startup.cpp src/arguments.cpp) add_library(Cheryl::DebugTerminal::Startup ALIAS ${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL}) cheryl_register_public_component(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} FEATURE CHERYL_HAS_SUPPORT_DEBUG_TERMINAL BUILD_ONLY) @@ -10,9 +10,12 @@ set_target_properties(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} PROPERTIES VERSION "${VERSION_PROJECT_ENGINE}" OUTPUT_NAME "${OUTPUT_LIB_ENGINE_S_DEBUG_TERMINAL}" ARCHIVE_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") +cheryl_configure_library(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} DEBUG_TERMINAL) target_compile_features(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} PUBLIC cxx_std_23) target_include_directories(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} PUBLIC "$" "$") target_compile_definitions(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} PRIVATE "CHERYL_TERMINAL_AUTOMATIC=$,1,0>") + +target_link_libraries(${TARGET_LIB_ENGINE_S_DEBUG_TERMINAL} PUBLIC Cheryl::Engine) diff --git a/projects/engine/support/debug-terminal/include/cheryl/core/debug-terminal/startup.h b/projects/engine/support/debug-terminal/include/cheryl/core/debug-terminal/startup.h index a0cfcdc0..eb121fe3 100644 --- a/projects/engine/support/debug-terminal/include/cheryl/core/debug-terminal/startup.h +++ b/projects/engine/support/debug-terminal/include/cheryl/core/debug-terminal/startup.h @@ -1,9 +1,11 @@ #pragma once +#include + namespace CE::Application::DebugTerminal { using Main = int (*)(int argc, char** argv); using Start = void (*)(); // The module owns its session and shutdown. No callback leaves startup untouched. - [[nodiscard]] int run(int argc, char** argv, Main application, Start start = nullptr); + [[nodiscard]] CE_DEBUG_TERMINAL_API int run(int argc, char** argv, Main application, Start start = nullptr); } diff --git a/projects/engine/support/logging-config/CMakeLists.txt b/projects/engine/support/logging-config/CMakeLists.txt index b001a4f9..0cc30c3f 100644 --- a/projects/engine/support/logging-config/CMakeLists.txt +++ b/projects/engine/support/logging-config/CMakeLists.txt @@ -1,7 +1,5 @@ # Public headers contain logging templates; every consumer must use the same policy. -set(CHERYL_LOG_PROFILE "auto" CACHE STRING "Logging defaults: auto, developer, support, or release") -set_property(CACHE CHERYL_LOG_PROFILE PROPERTY STRINGS auto developer support release) -set(CHERYL_LOG_COMPILED_MASK "" CACHE STRING "Optional severity-bit override (0x3f trace, 0x1f debug, 0x0f info, 0x07 warn, 0 off)") +cheryl_configure_logging_options() add_library(${TARGET_LIB_ENGINE_S_LOGGING_CONFIG} INTERFACE) if(CHERYL_LOG_PROFILE STREQUAL "auto") target_compile_definitions(${TARGET_LIB_ENGINE_S_LOGGING_CONFIG} INTERFACE diff --git a/projects/engine/support/signal-handlers/CMakeLists.txt b/projects/engine/support/signal-handlers/CMakeLists.txt index d1b670fe..f26fc0d0 100644 --- a/projects/engine/support/signal-handlers/CMakeLists.txt +++ b/projects/engine/support/signal-handlers/CMakeLists.txt @@ -1,8 +1,17 @@ -# Forward this object through the engine target so static consumers reliably get -# the original Debug crash bootstrap. Exception traces use Backward::Interface. +# Static consumers receive this object; a shared Engine embeds it once. +# Exception traces use Backward::Interface. file(GLOB_RECURSE cheryl_signal_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") add_library(${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS} OBJECT ${cheryl_signal_sources}) add_library(Cheryl::SignalHandlers ALIAS ${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS}) +if(CHERYL_BUILD_SHARED_LIBS) + set_target_properties(${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS} PROPERTIES + CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN YES) + target_compile_definitions(${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS} PRIVATE CE_BUILDING_ENGINE) +else() + target_compile_definitions(${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS} PRIVATE CE_ENGINE_STATIC) +endif() +target_include_directories(${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS} PRIVATE + "${CHERYL_REPOSITORY_ROOT}/projects/engine/include") target_compile_features(${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS} PUBLIC cxx_std_23) target_link_libraries(${TARGET_LIB_ENGINE_S_SIGNAL_HANDLERS} PUBLIC ${LINKAGE_PUBLIC_LIB_ENGINE_S_SIGNAL_HANDLERS}) diff --git a/projects/engine/support/signal-handlers/src/main.cpp b/projects/engine/support/signal-handlers/src/main.cpp index be1620c3..c9f2965a 100644 --- a/projects/engine/support/signal-handlers/src/main.cpp +++ b/projects/engine/support/signal-handlers/src/main.cpp @@ -1,7 +1,8 @@ -// Engine bootstrap: final consumers receive this object through Cheryl::Engine. +// Engine bootstrap: shared Engine owns it; static consumers receive the object. // Match the original NDEBUG scope; exception trace capture is independent. +#include #include #ifndef NDEBUG -backward::SignalHandling sh; +CE_API backward::SignalHandling sh; #endif -backward::TraceResolver tr; +CE_API backward::TraceResolver tr; diff --git a/projects/engine/support/startup/CMakeLists.txt b/projects/engine/support/startup/CMakeLists.txt index 9d15c518..27a2c78b 100644 --- a/projects/engine/support/startup/CMakeLists.txt +++ b/projects/engine/support/startup/CMakeLists.txt @@ -6,13 +6,14 @@ if(NOT TARGET CLI11::CLI11) add_subdirectory("${CHERYL_REPOSITORY_ROOT}/extern/cli11" extern/cli11 EXCLUDE_FROM_ALL) endif() -add_library(${TARGET_LIB_ENGINE_S_STARTUP} STATIC src/startup.cpp) +add_library(${TARGET_LIB_ENGINE_S_STARTUP} ${CHERYL_LIBRARY_TYPE} src/startup.cpp) add_library(Cheryl::Startup ALIAS ${TARGET_LIB_ENGINE_S_STARTUP}) cheryl_register_public_component(${TARGET_LIB_ENGINE_S_STARTUP} FEATURE CHERYL_HAS_SUPPORT_STARTUP) set_target_properties(${TARGET_LIB_ENGINE_S_STARTUP} PROPERTIES VERSION "${VERSION_PROJECT_ENGINE}" OUTPUT_NAME "${OUTPUT_LIB_ENGINE_S_STARTUP}" ARCHIVE_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") +cheryl_configure_library(${TARGET_LIB_ENGINE_S_STARTUP} STARTUP) target_include_directories(${TARGET_LIB_ENGINE_S_STARTUP} PUBLIC "$" "$") diff --git a/projects/engine/support/startup/include/cheryl/core/engine/startup.h b/projects/engine/support/startup/include/cheryl/core/engine/startup.h index fa461ff4..d09b52c4 100644 --- a/projects/engine/support/startup/include/cheryl/core/engine/startup.h +++ b/projects/engine/support/startup/include/cheryl/core/engine/startup.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -52,7 +54,7 @@ namespace CE::Application { * @param game Simulation hooks retained by reference. * @return A runtime using the selected mode, polling and timing policy. */ - [[nodiscard]] CE::Runtime::GameRuntime make_runtime(CE::Game::AbstractGame& game) const&; + [[nodiscard]] CE_STARTUP_API CE::Runtime::GameRuntime make_runtime(CE::Game::AbstractGame& game) const&; CE::Runtime::GameRuntime make_runtime(CE::Game::AbstractGame& game) const&& = delete; }; @@ -74,7 +76,11 @@ namespace CE::Application { * @param description Application description used by command-line help. * @param configuration Initial defaults before command-line overrides. */ - explicit Startup(StartupBackend backend, std::string description = "Cheryl application", StartupOptions configuration = {}); + CE_STARTUP_API explicit Startup( + StartupBackend backend, + std::string description = "Cheryl application", + StartupOptions configuration = {} + ); // Parser callbacks bind this object's configuration; its address is stable. Startup(const Startup&) = delete; @@ -87,7 +93,7 @@ namespace CE::Application { * initialize(). Empty definitions or registration after initialization fail. * @param definitions Callback receiving the application's option group. */ - void add_application_options(const OptionDefinitions& definitions); + CE_STARTUP_API void add_application_options(const OptionDefinitions& definitions); /** Parse, validate and construct a context once on the platform owner. * Command-line help/errors and invalid configuration return without a context. * Reuse and malformed argument arrays are rejected; other backend failures propagate. @@ -95,10 +101,10 @@ namespace CE::Application { * @param argv Nonnull array of argc nonnull argument strings. * @return An owned context and runtime policy, or an exit code without a context. */ - [[nodiscard]] StartupResult initialize(int argc, const char* const* argv); + [[nodiscard]] CE_STARTUP_API StartupResult initialize(int argc, const char* const* argv); private: - void add_engine_options(); - void validate_configuration() const; + CE_STARTUP_API void add_engine_options(); + CE_STARTUP_API void validate_configuration() const; }; } diff --git a/projects/engine/support/worker-topology/CMakeLists.txt b/projects/engine/support/worker-topology/CMakeLists.txt index b407c2ff..e05e0ca4 100644 --- a/projects/engine/support/worker-topology/CMakeLists.txt +++ b/projects/engine/support/worker-topology/CMakeLists.txt @@ -1,7 +1,7 @@ if(CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC AND NOT TARGET Cheryl::WorkerTopology::Hwloc) include(cmake/Hwloc.cmake) - add_library(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC} STATIC src/hwloc.cpp) + add_library(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC} ${CHERYL_LIBRARY_TYPE} src/hwloc.cpp) add_library(Cheryl::WorkerTopology::Hwloc ALIAS ${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC}) cheryl_register_public_component(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC} FEATURE CHERYL_HAS_SUPPORT_WORKER_TOPOLOGY_HWLOC) @@ -9,6 +9,7 @@ if(CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC AND NOT TARGET Cheryl::WorkerTopology::Hwl VERSION "${VERSION_PROJECT_ENGINE}" OUTPUT_NAME "${OUTPUT_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC}" ARCHIVE_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") + cheryl_configure_library(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC} WORKER_TOPOLOGY_HWLOC) target_include_directories(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_HWLOC} PUBLIC "$" "$") @@ -18,7 +19,7 @@ if(CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC AND NOT TARGET Cheryl::WorkerTopology::Hwl endif() if(CHERYL_BUILD_WORKER_TOPOLOGY_JSON AND NOT TARGET Cheryl::WorkerTopology::Json) - add_library(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON} STATIC src/configuration.cpp) + add_library(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON} ${CHERYL_LIBRARY_TYPE} src/configuration.cpp) add_library(Cheryl::WorkerTopology::Json ALIAS ${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON}) cheryl_register_public_component(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON} FEATURE CHERYL_HAS_SUPPORT_WORKER_TOPOLOGY_JSON) @@ -26,6 +27,7 @@ if(CHERYL_BUILD_WORKER_TOPOLOGY_JSON AND NOT TARGET Cheryl::WorkerTopology::Json VERSION "${VERSION_PROJECT_ENGINE}" OUTPUT_NAME "${OUTPUT_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON}" ARCHIVE_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") + cheryl_configure_library(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON} WORKER_TOPOLOGY_JSON) target_include_directories(${TARGET_LIB_ENGINE_S_WORKER_TOPOLOGY_JSON} PUBLIC "$" "$") diff --git a/projects/engine/support/worker-topology/cmake/Hwloc.cmake b/projects/engine/support/worker-topology/cmake/Hwloc.cmake index c64d3d7f..1d89b2d9 100644 --- a/projects/engine/support/worker-topology/cmake/Hwloc.cmake +++ b/projects/engine/support/worker-topology/cmake/Hwloc.cmake @@ -1,5 +1,5 @@ # This file is included only for an explicitly selected provider. Never fetch an SDK. -set(CHERYL_HWLOC_TARGET "" CACHE STRING "Supplied hwloc 2.5+ (2.x) SDK target") +cheryl_configure_hwloc_options() set(cheryl_hwloc_target "${CHERYL_HWLOC_TARGET}") if(cheryl_hwloc_target AND NOT TARGET "${cheryl_hwloc_target}") message(FATAL_ERROR "CHERYL_HWLOC_TARGET must name an existing target: ${cheryl_hwloc_target}") diff --git a/projects/engine/support/worker-topology/include/cheryl/core/engine/worker-topology/configuration.h b/projects/engine/support/worker-topology/include/cheryl/core/engine/worker-topology/configuration.h index 272d145a..cfa2ab0b 100644 --- a/projects/engine/support/worker-topology/include/cheryl/core/engine/worker-topology/configuration.h +++ b/projects/engine/support/worker-topology/include/cheryl/core/engine/worker-topology/configuration.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -9,7 +11,9 @@ namespace CE::Workers::Json { // Version 1.0 settings; no SDK types or discovery, threads, group creation or // affinity changes. Runtime availability is resolved by CE::Workers::Pool/make_group. - [[nodiscard]] CE::Workers::Configuration parse_configuration(std::string_view document, const std::filesystem::path& source = {}); - [[nodiscard]] CE::Workers::Configuration load_configuration(std::istream& input, const std::filesystem::path& source = {}); - [[nodiscard]] CE::Workers::Configuration load_configuration(const std::filesystem::path& source); + [[nodiscard]] CE_WORKER_TOPOLOGY_JSON_API CE::Workers::Configuration + parse_configuration(std::string_view document, const std::filesystem::path& source = {}); + [[nodiscard]] CE_WORKER_TOPOLOGY_JSON_API CE::Workers::Configuration + load_configuration(std::istream& input, const std::filesystem::path& source = {}); + [[nodiscard]] CE_WORKER_TOPOLOGY_JSON_API CE::Workers::Configuration load_configuration(const std::filesystem::path& source); } diff --git a/projects/engine/support/worker-topology/include/cheryl/core/engine/worker-topology/hwloc.h b/projects/engine/support/worker-topology/include/cheryl/core/engine/worker-topology/hwloc.h index 8ae4d638..222e0dfe 100644 --- a/projects/engine/support/worker-topology/include/cheryl/core/engine/worker-topology/hwloc.h +++ b/projects/engine/support/worker-topology/include/cheryl/core/engine/worker-topology/hwloc.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -7,5 +9,5 @@ namespace CE::Workers::Hwloc { // Discover the executing machine when called; does not bind the creator or // place memory. Retain this instance in CE::Workers::PoolOptions::topology_provider. - [[nodiscard]] std::shared_ptr make_topology_provider(); + [[nodiscard]] CE_WORKER_TOPOLOGY_HWLOC_API std::shared_ptr make_topology_provider(); } diff --git a/projects/engine/tests/consumer/CMakeLists.txt b/projects/engine/tests/consumer/CMakeLists.txt index 2b568328..1fca0032 100644 --- a/projects/engine/tests/consumer/CMakeLists.txt +++ b/projects/engine/tests/consumer/CMakeLists.txt @@ -7,12 +7,17 @@ if(NOT TARGET Cheryl::Engine) block(SCOPE_FOR VARIABLES) set(CHERYL_BUILD_TESTS OFF) set(CHERYL_BUILD_DEMO OFF) + set(CHERYL_BUILD_DOCS OFF) + set(CHERYL_BUILD_SDK OFF) set(CHERYL_BUILD_CONSUMER_TESTS 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) + set(CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC OFF) + set(CHERYL_BUILD_WORKER_TOPOLOGY_JSON OFF) add_subdirectory("${CHERYL_REPOSITORY_ROOT}" engine EXCLUDE_FROM_ALL) endblock() endif() diff --git a/projects/engine/tests/consumer/src/main.cpp b/projects/engine/tests/consumer/src/main.cpp index 207b2ec4..b0f49002 100644 --- a/projects/engine/tests/consumer/src/main.cpp +++ b/projects/engine/tests/consumer/src/main.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -12,6 +13,9 @@ bool exercise_resources(); int main() { + CE::Rendering::Camera2D camera; + camera.set_framebuffer_size({640, 480}); + const bool camera_size = camera.framebuffer_size() == CE::Platform::FramebufferSize{640, 480}; CE::Events::EventBus events; int observed = 0; const auto registration = events.register_listener("consumer", [&](const std::any& value) { observed = std::any_cast(value); }); @@ -36,5 +40,5 @@ int main() { std::string_view{CE::Logging::Categories::assets} == "assets" && std::string_view{CE::Logging::Categories::memory} == "memory" && std::string_view{CE::Logging::Categories::cheryl} == "cheryl"; - return observed == 7 && typed_observed == 9 && value.get() == 11 && exercise_resources() && log_names ? 0 : 1; + return observed == 7 && typed_observed == 9 && value.get() == 11 && exercise_resources() && log_names && camera_size ? 0 : 1; } diff --git a/projects/engine/tests/unit/src/font-selection.cpp b/projects/engine/tests/unit/src/font-selection.cpp index bed32897..cfd97d51 100644 --- a/projects/engine/tests/unit/src/font-selection.cpp +++ b/projects/engine/tests/unit/src/font-selection.cpp @@ -6,8 +6,10 @@ #include #include #include +#include #include #include +#include #include namespace { @@ -26,11 +28,19 @@ namespace { std::filesystem::path path; inline static std::atomic next{0}; - FontDirectory() - : path(std::filesystem::temp_directory_path() / ("cheryl-font-" + - std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + "-" + std::to_string(next++))) { - if (!std::filesystem::create_directory(path)) - throw std::runtime_error("Font fixture directory already exists"); + FontDirectory() { + const auto root = std::filesystem::temp_directory_path(); + std::random_device random; + for (unsigned attempt = 0; attempt < 64; ++attempt) { + path = root / ("cheryl-font-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + "-" + + std::to_string(random()) + "-" + std::to_string(next++)); + std::error_code error; + if (std::filesystem::create_directory(path, error)) + return; + if (error && error != std::errc::file_exists) + throw std::filesystem::filesystem_error("Unable to create font fixture directory", path, error); + } + throw std::runtime_error("Unable to create a unique font fixture directory"); } ~FontDirectory() { std::error_code error; @@ -244,6 +254,7 @@ TEST(font_selection, corrupt) { } auto selection = isolated_selection(); for (const auto& path : {corrupt, empty, directory.path}) { + SCOPED_TRACE(path.string()); selection.preferred = {CE::Text::FontFile{path}}; EXPECT_THROW(static_cast(CE::Text::FontCollection::load(selection)), CE::Exceptions::runtime_exception); } diff --git a/projects/modules/README.md b/projects/modules/README.md index 768156c9..b10d58b8 100644 --- a/projects/modules/README.md +++ b/projects/modules/README.md @@ -27,15 +27,16 @@ modules/ | [ui/rmlui](ui/rmlui/README.md) | `module_ui_rmlui` | `Cheryl::UI::RmlUi` | `CHERYL_BUILD_UI_RMLUI` | Native document sessions, routed input and premultiplied retained rendering through Engine contracts. | | [audio/miniaudio](audio/miniaudio/README.md) | `module_audio_miniaudio` | `Cheryl::Audio::Miniaudio` | `CHERYL_BUILD_AUDIO_MINIAUDIO` | [`audio`](../engine/include/cheryl/audio): immutable `Clip`, synchronized `Voice` and application-owned `System`. | -Native GLFW, OpenGL and TGUI are selected by default. RmlUi defaults to `OFF`. -Miniaudio is independently selectable and defaults to `OFF`; it links only Engine -and its private SDK, with no window or graphics requirement. -The Linux Debug terminal is selected by default on Linux GNU/Clang; its `AUTO` -policy links the implementation only in Debug. Application/test composition selects -it without adding a reverse Engine dependency. Other platforms omit it. +Native GLFW, OpenGL, TGUI, RmlUi and miniaudio are selected by default. +Miniaudio links only Engine and its private SDK, with no window or graphics +requirement. +The Linux Debug terminal defaults to `ON` for single-configuration Linux GNU/Clang +Debug builds. Explicit `ON` includes it in every configuration from that build +directory. Application/test composition selects it without adding a reverse Engine +dependency. Other platforms omit it. OpenGL requires Native GLFW. Each UI owner links only Engine and its own toolkit; -either or both can be selected. Disable all module selection options for -Engine alone. Link the public targets to inherit their include directories and +either or both can be selected. For Engine alone, disable all module and both +worker-support options; see the [engine-only configuration](../../docs/development/building.md#engine-only-configuration). Link the public targets to inherit their include directories and dependency requirements; each owner also supplies a standalone CMake entry point. Command-line applications can also link `Cheryl::Startup` or diff --git a/projects/modules/audio/miniaudio/CMakeLists.txt b/projects/modules/audio/miniaudio/CMakeLists.txt index 66272f47..0929a3c4 100644 --- a/projects/modules/audio/miniaudio/CMakeLists.txt +++ b/projects/modules/audio/miniaudio/CMakeLists.txt @@ -7,10 +7,10 @@ endif() if(TARGET Cheryl::Audio::Miniaudio) return() endif() -cheryl_option(CHERYL_BUILD_TESTS "Build module-local checks" OFF) +cheryl_configure_module_options() cheryl_prepare_module() -set(CHERYL_MINIAUDIO_SOURCE "" CACHE PATH "Miniaudio 0.11.25 source directory, overriding the pinned submodule") +cheryl_configure_miniaudio_options() if(TARGET miniaudio::miniaudio) set(cheryl_miniaudio_target miniaudio::miniaudio) elseif(TARGET miniaudio) @@ -29,6 +29,9 @@ else() set_target_properties(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO_S_SDK} PROPERTIES OUTPUT_NAME "${OUTPUT_LIB_MODULE_AUDIO_MINIAUDIO_S_SDK}" ARCHIVE_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") + if(CHERYL_BUILD_SHARED_LIBS) + set_target_properties(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO_S_SDK} PROPERTIES C_VISIBILITY_PRESET hidden) + endif() target_include_directories(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO_S_SDK} SYSTEM PUBLIC "${cheryl_miniaudio_source}") target_link_libraries(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO_S_SDK} PRIVATE ${LINKAGE_PRIVATE_LIB_MODULE_AUDIO_MINIAUDIO_S_SDK} ${CMAKE_DL_LIBS}) @@ -39,13 +42,14 @@ else() endif() file(GLOB_RECURSE cheryl_audio_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") -add_library(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO} STATIC ${cheryl_audio_sources}) +add_library(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO} ${CHERYL_LIBRARY_TYPE} ${cheryl_audio_sources}) add_library(Cheryl::Audio::Miniaudio ALIAS ${TARGET_LIB_MODULE_AUDIO_MINIAUDIO}) cheryl_register_public_component(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO} FEATURE CHERYL_HAS_AUDIO_MINIAUDIO) set_target_properties(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO} PROPERTIES VERSION "${VERSION_PROJECT_MODULE_AUDIO_MINIAUDIO}" OUTPUT_NAME "${OUTPUT_LIB_MODULE_AUDIO_MINIAUDIO}" ARCHIVE_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") +cheryl_configure_library(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO} AUDIO_MINIAUDIO) target_include_directories(${TARGET_LIB_MODULE_AUDIO_MINIAUDIO} PUBLIC "$" "$") diff --git a/projects/modules/audio/miniaudio/README.md b/projects/modules/audio/miniaudio/README.md index 31dbc9a8..5f23ff03 100644 --- a/projects/modules/audio/miniaudio/README.md +++ b/projects/modules/audio/miniaudio/README.md @@ -14,8 +14,8 @@ Select `CHERYL_BUILD_AUDIO_MINIAUDIO=ON` at the repository root and link: target_link_libraries(game PRIVATE Cheryl::Engine Cheryl::Audio::Miniaudio) ``` -This module defaults to `OFF`, preserving existing Engine-only and graphical -assemblies until their applications select audio. It has a standalone entry point: +This module defaults to `ON` at the repository root. Disable it for assemblies +that do not need audio. It has a standalone entry point: set `CHERYL_REPOSITORY_ROOT` to an absolute checkout path before adding the owner. It reuses `Cheryl::Engine` or bootstraps Engine through the common module helper. @@ -96,7 +96,7 @@ narrow it for a focused change and avoid repeating cases through aggregates. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -DCMAKE_CXX_SCAN_FOR_MODULES=OFF \ -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST \ - -DCHERYL_LOG_PROFILE=developer -DCHERYL_SANDBOX_BUILD=OFF \ + -DCHERYL_LOG_PROFILE=developer \ -DCHERYL_BUILD_NATIVE_GLFW=OFF -DCHERYL_BUILD_OPENGL=OFF \ -DCHERYL_BUILD_UI_TGUI=OFF -DCHERYL_BUILD_UI_RMLUI=OFF \ -DCHERYL_BUILD_AUDIO_MINIAUDIO=ON -DCHERYL_MINIAUDIO_SOURCE= \ diff --git a/projects/modules/audio/miniaudio/include/cheryl/backends/miniaudio.h b/projects/modules/audio/miniaudio/include/cheryl/backends/miniaudio.h index ad23b79f..69818e32 100644 --- a/projects/modules/audio/miniaudio/include/cheryl/backends/miniaudio.h +++ b/projects/modules/audio/miniaudio/include/cheryl/backends/miniaudio.h @@ -1,5 +1,7 @@ #pragma once +#include + #include
{cpp:class}`CE::Audio::System` and {cpp:class}`CE::Audio::Voice`: synchronized playback/control, an independent output clock, maintenance and safe closed handles. The application owns output lifetime. [Audio ownership](../runtime/audio.md); [miniaudio](../modules/miniaudio.md). | +| UI toolkit
Consume neutral input records/focus, copied dimensions, CPU font/image services and immutable resource/frame publication. Each toolkit owns its session and native authoring API; there is no common Engine widget/session base to implement. [UI bridge responsibilities](../development/ui-adapters.md#bridge-responsibilities); [TGUI](../modules/tgui.md), [RmlUi](../modules/rmlui.md). | +| Queued event delivery
{cpp:type}`CE::Events::EventBus::Delivery` and {cpp:type}`CE::Events::EventBus::ErrorHandler`: own accepted work without inline execution, preserve a listener's FIFO stream and report failure/cancellation. Supplied adapters target platform, simulation or a worker stream. [Queued delivery](../runtime/event-delivery.md#optional-queued-delivery); [worker cancellation](../runtime/event-delivery.md#worker-stream-acceptance-and-cancellation). | +| Startup/configuration
Compose compatible adapters and validate configuration before the runtime session. Startup integrates selected backends; worker JSON loading supplies configuration rather than another executor. [Startup](../modules/startup.md), [Workers](../modules/workers.md); [source/SDK composition](../development/consuming-engine.md). | +| Process output attachment
Supply the optional Debug terminal support callback and own its native viewer/process lifetime. This source-only attachment sits outside the context adapter graph. [Linux Debug terminal](../modules/debug-terminal-linux.md); [logging output](../runtime/logging.md#native-output-terminal). | + +## Replacing a platform integration + +For an SDL3 replacement, the platform work maps to `DisplaySystem`, `Window` and +`Input::System`. Keeping Cheryl's OpenGL renderer also requires an OpenGL +`Context` bridge and compatible resource lifetime. The current OpenGL target and +startup factory select Native GLFW explicitly; a new platform therefore needs +composition/build selection changes as well as interface implementations. The +[module selection](assembling-engine.md), [OpenGL owner](../modules/opengl.md) and +[startup guide](../modules/startup.md) describe that current coupling. + +Keyboard, mouse-button and gamepad enums belong to Engine and are accepted +directly by bindings; see +[control identities and binding tokens](../runtime/input-state-model.md#control-identities-and-binding-tokens). +A replacement normalizes its native codes to those standard control IDs before +binding evaluation. It must also fulfill polling, capture, routing and native +failure contracts, and state which device capabilities it supports. Backend +selection still determines available devices and observations. + +## Implement and connect a replacement + +1. Select the responsibilities you are replacing in the catalogue. Keep the + portable interfaces in Engine and the concrete implementation in its module. +2. Use the implementation column above to identify cooperating owners. Replace + or bridge every affected domain before creating a runtime context. +3. Implement the required operations and report optional capabilities explicitly. + Unsupported requests reject instead of silently losing behavior. +4. Assemble the components through a factory, preserving their ownership and + initialization dependencies. Use the existing module's independent consumer + and the architecture validation procedures as conformance examples. + +The [assembly guide](assembling-engine.md) explains factories and facades; +[module authoring](../development/modules.md) covers target registration and +independent consumers. Runtime-owned and application-owned objects follow +[ownership and cleanup](ownership-cleanup.md). diff --git a/docs/architecture/ownership-cleanup.md b/docs/architecture/ownership-cleanup.md new file mode 100644 index 00000000..4407db4c --- /dev/null +++ b/docs/architecture/ownership-cleanup.md @@ -0,0 +1,79 @@ +# Ownership and Cleanup + +Decide what an application component owns, what it borrows, and which work must +finish before its dependencies can be destroyed. A C++ handle's lifetime and a +native backend's usable lifetime are separate obligations. + +## Choose what to retain + +| Relationship | Application responsibility | Cheryl examples | +| --- | --- | --- | +| Own an object | Control destruction and finish its callers before releasing it. | A Startup result owns its context; the application owns its game, GUI session and audio system. | +| Borrow an object | Keep its owner alive and obey the object's access rules. | A runtime borrows its context/game; context accessors and display-owned windows return borrowed access. | +| Retain an immutable result | Keep a strong handle for as long as that version is needed. | Assets, input poll snapshots, uploaded text and frame packets retain their selected data. | +| Hold a scoped request | Retain the lease while the request is wanted; release it when that consumer finishes. | Input Events/Text capture and keyboard focus. | +| Save a submission endpoint | Own callback inputs or keep borrowed captures alive through completion or cancellation. | Dispatcher submissions remain safe to call after closure, but reject new work and do not retain the runtime. | + +For example, two game objects can retain the same sprite while keeping independent +positions and animation cursors. Dropping one object's handle releases only that +owner. A manager cache or published frame can still retain the sprite's resources. +The [resource guide](../resources/resource-residency.md#cache-ownership) explains +which owners to inspect before treating continued residency as a leak. + +## Use a lease for a temporary input request + +With a live input system and an application-selected nonzero `editor_target`: + +```cpp +auto events = input.capture(CE::Input::Capture::Channel::Events); +auto text = input.capture(CE::Input::Capture::Channel::Text); +auto focus = input.routing().focus(editor_target); +// Retain these handles while the editor receives input. +// During update, read tick.input.records_for(editor_target) in order. +focus.reset(); +text.reset(); +events.reset(); +``` + +Keep the handles as editor members when the request spans updates. Their +destructors also release the requests. An older focus lease cannot clear a newer +request. These leases retain request state, not an input adapter, window or widget; +capture and focus take effect at a subsequent poll. Toolkit sessions manage their +own leases through their focus methods. The +[input guide](../runtime/input-state-model.md#focus-routing-and-editing-controls) +defines poll epochs and already captured records. + +## Keep native domains alive for native use + +A retained draw packet keeps geometry, material and image generations alive. It +does not keep a destroyed graphics context usable. Likewise, an audio voice handle +can survive system closure but then reports `Closed`; controls reject. + +CPU release state can have a different lifetime. Pooled object handles retain +their return/construction contexts, allowing final release after a pool facade +dies. Their own callers still need to finish before that facade is destroyed. +Use [CPU memory lifetime](../resources/resource-lifetime.md) when allocating +application objects, and the backend's native retirement contract for GPU handles. + +## Shut down in dependency order + +The runtime provides the sequence below for context-owned work. Application-owned +services join that sequence through the game's hooks: + +| Boundary | What the application does | +| --- | --- | +| Request stop | Return from the current update when no further game work is needed. A stop request does not interrupt an executing callback. | +| Simulation joins | Finish game updates and frame preparation before transferring UI teardown to platform. | +| `quiesce()` | Stop external producers and invalidate borrowed event listeners while their targets remain alive. Avoid waiting for work that still needs platform dispatch. | +| Accepted work settles | Keep worker inputs, upload dependencies and callback targets alive while platform continues servicing accepted dependencies. | +| Frames recycle and `deinit()` runs | Release game resources and application-held toolkit objects; close GUI sessions after UI quiescence. Settle and close separately owned audio services. | +| Adapters close | Input detaches from a live window; native resources retire before graphics/window destruction. | + +Cleanup pairs with attempted initialization, so retain only successfully acquired +handles and make cleanup tolerate partial startup. For borrowed `[this]` event +callbacks, use `unregister_and_wait()` before destroying the target; ordinary +unregister prevents new entry but permits an already running callback to finish. + +The [runtime lifecycle](../runtime/runtime-frame-boundary.md#shutdown) owns the +exact shutdown and failure order. [Threads and Data Flow](threads-data-flow.md) +explains which dependencies require continued owner-thread progress. diff --git a/docs/architecture/threads-data-flow.md b/docs/architecture/threads-data-flow.md new file mode 100644 index 00000000..92c8ae93 --- /dev/null +++ b/docs/architecture/threads-data-flow.md @@ -0,0 +1,86 @@ +# Threads and Data Flow + +Choose where work runs, give that work stable inputs, and publish owned results +at the next appropriate boundary. Sequential and concurrent runtime modes use the +same ownership rules; concurrent mode makes crossing them explicit. + +## Choose where work runs + +| Work | Execution owner | Transfer mechanism | +| --- | --- | --- | +| Poll native input, operate a window, upload, draw or present | Platform/graphics caller of `run()` | Platform dispatcher for requests from other owners. | +| Change authoritative game state or prepare a frame | Simulation; the calling thread in sequential mode or one dedicated thread in concurrent mode | Copied ticks, simulation dispatcher and completed result adoption. | +| Decode files, shape text or perform independent CPU preparation | Worker pool | A group submission with owned inputs and a future result. | +| Advance audio playback | Audio implementation's output clock | Synchronized audio controls; close it separately from the runtime adapters. | + +The dedicated simulation thread is separate from general worker capacity. A CPU +worker has no graphics affinity. Toolkit widgets and documents belong to their +simulation/UI owner; native callbacks collect observations instead of calling them. +The [Engine Systems overview](../engine-systems/index.md#execution) maps these +owners to their systems. + +## Choose how work is delivered + +| Need | Use | Completion and ordering | +| --- | --- | --- | +| Perform an operation on its current owner | A direct call | It completes or throws before returning; the caller supplies valid access. | +| Reach the platform or simulation owner | A dispatcher submission | Always queued, with a future; one detached FIFO batch drains at an owner boundary. | +| Use independent CPU capacity | A worker-group job | A future reports its outcome; parallel jobs can finish out of submission order. | +| Notify registered consumers | A typed event channel | Immediate listeners run on the producer; queued listeners select delivery and an independent error sink. | +| Publish only the latest value | A synchronized snapshot or immutable generation | Readers retain a stable version; intermediate versions can be superseded. | + +Use an event when subscribers should react to an occurrence. Use a latest-value +snapshot when a consumer needs current state rather than every occurrence. An +empty queue or a diagnostic count does not prove that detached work has finished. + +## Prepare and publish a replacement + +Asset loading, text updates and UI publication share this sequence: + +```text +Simulation: request replacement; keep the current version + -> Worker: prepare owned CPU data + -> Platform: upload and return a complete candidate + -> Simulation update: adopt the ready candidate + -> Frame preparation: copy values and retain selected resources + -> Platform: render and recycle the completed frame +``` + +A worker-to-platform request carries CPU data and retained inputs, not a pointer +to mutable game state. The platform callback returns a candidate instead of +modifying simulation through a captured `this`. Simulation checks readiness before +calling `get()` and handles any future error according to the game's failure policy. +Bound outstanding replacements to avoid an ever-growing queue of obsolete work. + +For [text replacement](../getting-started/how-to/update-text.md), old frames retain +their matching text geometry and atlas while a newer generation is adopted. For +[asset batches](../assets/asset-loading.md#publication-and-retry), upload can publish +entries incrementally; a failed batch does not roll back earlier publications. +Choose the application's adoption boundary separately from the cache's behavior. + +## Keep readers independent of live producers + +Input delivers immutable observations; the tick's borrowed views last for that +update. Copy owned data for longer use. Frame preparation copies camera matrices, +transforms and selected animation cells so rendering needs no live entity or cursor. +GUI recordings own pixels and triangles so upload needs no live widget tree. + +Immutable snapshots and resource generations solve the same lifetime problem: +consumers keep the version they were given while the producer prepares another. +They do not synchronize mutable objects that remain outside the snapshot. + +## Preserve progress during waits and shutdown + +Never block an owner on a future that requires that owner to complete it. A worker +can also strand its children by waiting on jobs in the same saturated pool. +Simulation dispatch waits for the next scheduled update; posting does not invent +an early tick. Worker FIFO selection alone does not guarantee callback completion +order; queued event delivery supplies a serial stream where that is required. + +During shutdown, stop producers while their targets remain alive and let accepted +dependencies settle before releasing them. Runtime continues pumping platform +dependencies while joining simulation and draining its tracked groups. Follow +[Ownership and Cleanup](ownership-cleanup.md#shut-down-in-dependency-order) for +application-owned services. The detailed [dispatch](../runtime/thread-dispatch.md), +[worker](../runtime/worker-execution.md) and [event](../runtime/event-delivery.md) +guides define cancellation, closure and delivery guarantees. diff --git a/docs/assets/asset-loading.md b/docs/assets/asset-loading.md index c8d82fcc..358ae25c 100644 --- a/docs/assets/asset-loading.md +++ b/docs/assets/asset-loading.md @@ -68,12 +68,41 @@ auto preparing = workers.submit([loader, platform] { // returned upload future. Check that future's readiness before reading metadata. ``` +Retain the preparation future as game state. Also keep the upload future and the +last accepted metadata snapshot between updates; include `` and ``: + +```cpp +using Metadata = decltype(loader->manifests()); +std::future uploading; +Metadata published_definitions = loader->manifests(); +``` + +During later `update()` calls: + +```cpp +if (preparing.valid() && preparing.wait_for(std::chrono::seconds{0}) == std::future_status::ready) + uploading = preparing.get(); +if (uploading.valid() && uploading.wait_for(std::chrono::seconds{0}) == std::future_status::ready) + published_definitions = uploading.get(); +``` + +The first `get()` transfers the platform-upload future; the second adopts the +successful metadata snapshot. Exceptions reach the update's selected failure +policy and leave the prior snapshot unchanged. Retrieve the required new asset +handles before making application state depend on that content. Ordinary loading +preserves existing keys; this sequence does not turn it into image/sprite hot reload. +Start another batch only after both futures are invalid, keeping one batch pending. + An owned root or injected shared root supplies physical capacity; this group belongs to the context shutdown domain. Accepted CPU jobs finish while the platform dispatcher remains available. Any final unexecuted upload is cancelled before resource teardown, so its future reports failure instead of hanging. Use platform submission endpoints rather than borrowed dispatcher pointers. +[Threads and Data Flow](../architecture/threads-data-flow.md#prepare-and-publish-a-replacement) +explains this handoff across systems. Keep CPU inputs and upload dependencies +alive through completion or cancellation instead of blocking their execution owner. + ## Publication and retry `CE::Assets::Loading::Prepared` owns decoded pixels, shader bytes and definitions, with no provider or native diff --git a/docs/assets/asset-manifests.md b/docs/assets/asset-manifests.md index 2f9aa732..c5d3aeb2 100644 --- a/docs/assets/asset-manifests.md +++ b/docs/assets/asset-manifests.md @@ -7,6 +7,62 @@ and [shaders/materials](../../assets/schemas/graphics/shader-manifest-2.0.schema Earlier versions are rejected rather than implicitly upgraded. `$schema` is an editor hint; the runtime neither fetches it nor discovers schemas as assets. +## Add a sprite sheet + +Start with the maintained [First Game asset root](../../projects/apps/first-game/assets/). +Copy that directory into your application's assets so its image, shader sources +and material recipe stay together. Its relevant layout is: + +```text +assets/ + graphics/ + graphics-manifests.json + definitions/sprites.json + textures/actor.png + shaders/materials.json + shaders/first-game.vert + shaders/first-game.frag +``` + +The complete index selects the sprite definition and its drawing materials: + +```{literalinclude} ../../projects/apps/first-game/assets/graphics/graphics-manifests.json +:language: json +``` + +The complete sprite document describes the actor's two 16-by-16 cells: + +```{literalinclude} ../../projects/apps/first-game/assets/graphics/definitions/sprites.json +:language: json +``` + +Both `definitions/sprites.json` and `textures/actor.png` resolve from `graphics/`, +even though the definition itself is in `graphics/definitions/`. The namespace +and sprite map key combine into `first-game:actor`. During platform-owned game +initialization, after the provider is ready: + +```cpp +CE::Assets::Loading::Loader loader(asset_root); +loader.load_assets(engine.resources()); +auto actor = CE::Assets::Sprites::Manager::get().get_asset("first-game", "actor"); +if (!actor) + throw std::runtime_error("The actor definition was not loaded"); +``` + +Include the loader and sprite-manager headers plus ``; `asset_root` +is the copied assets directory and `engine` is the initialized runtime context. +The [sprite task](../getting-started/how-to/draw-sprite.md) supplies the matching +material and frame submission. Cell zero selects the left frame and cell one +the right. The image must contain both cells; preparation rejects a grid whose +occupied bounds exceed its decoded image. + +To add your own sheet, copy a definition entry, choose a unique namespace/name, +and change its texture path, frame size, rows, columns, spacing and pivot. Add the +document to the index. If retrieval returns null, check the qualified identity and +index selection; unlisted JSON is not loaded. Keep `shaders/materials.json` and +its source files when using the maintained image/text materials. The field +contracts below cover larger atlases, clips, autotiles and custom shaders. + ## Index selection and paths Only the exact basename `graphics-manifests.json` selects definition documents. diff --git a/docs/assets/asset-values-and-playback.md b/docs/assets/asset-values-and-playback.md index 517564dc..fd0ab7af 100644 --- a/docs/assets/asset-values-and-playback.md +++ b/docs/assets/asset-values-and-playback.md @@ -135,6 +135,86 @@ after choosing the original target. ## Tile selection +### Sample an application map + +This CPU example maps a small game-owned terrain grid to a four-neighbor rule. +Rows increase southward and columns eastward; rendering placement is a separate +game choice. Terrain `0` is empty and `1` is the terrain being connected. Include +``, ``, ``, +`` and ``: + +```cpp +namespace Tiles = CE::Assets::Tilesets; +namespace Pick = CE::Assets::Tilesets::Selection; +using Cell = CE::Assets::Grids::CellIndex; + +const std::array, 3> map{{ + {{0, 1, 0}}, + {{1, 1, 0}}, + {{0, 0, 0}} +}}; +Tiles::BitmaskRule rule; +rule.type = Tiles::BitmaskType::FourNeighbor; +rule.bit_order = {Tiles::Direction::North, Tiles::Direction::East, + Tiles::Direction::South, Tiles::Direction::West}; +rule.cases = {{0, 0}, {9, 1}, {15, 2}}; + +const int column = 1; +const int row = 1; +const Pick::Sampler sampler = [&](Pick::Site site) -> Pick::Sample { + int x = column; + int y = row; + switch (site.direction) { + case Tiles::Direction::North: --y; break; + case Tiles::Direction::East: ++x; break; + case Tiles::Direction::South: ++y; break; + case Tiles::Direction::West: --x; break; + default: return {Pick::SampleState::Unknown, 0}; + } + if (x < 0 || x >= 3 || y < 0 || y >= 3) + return {Pick::SampleState::Outside, 0}; + return {Pick::SampleState::Known, map[y][x]}; +}; +Pick::Options options; +options.terrain = map[row][column]; +options.outside = Pick::Fallback::Empty; +const auto result = Pick::select(rule, sampler, options); +std::optional visible_cell; +if (const auto* cell = std::get_if(&result)) + visible_cell = *cell; +``` + +For the center tile, North and West connect: bits zero and three produce mask +`9`, selecting cell `1`. This example deliberately declares only three masks. +Selecting the top-middle tile produces mask `4`, absent from `cases`, and returns +`MissingRule`; `visible_cell` stays empty and the game skips that tile. Add the +remaining authored cases for a complete terrain set. If a required neighbor is +temporarily unavailable, return `Unknown` rather than `Outside`; the default +unknown policy produces `IncompleteNeighborhood` instead of inventing empty land. + +Keep this rule and map on their simulation owner. Save the selected cell in the +game's visible state; its sampler borrows the map only during selection. A matching +loaded tileset must have the selected cell ranges and artwork. For that retained +`tileset`, a nonnegative simulation time in milliseconds named `elapsed`, and the +`pass`, `style` and `context` from ordinary frame submission: + +```cpp +if (visible_cell) { + const auto cell = tileset.cell_at(*visible_cell, elapsed); + pass.add(CE::Rendering::Submission::resolve_tile(tileset, cell, style, context)); +} +``` + +`cell_at()` substitutes the selected target's animation once. Advance `elapsed` +during update, so skipped frame preparation does not pause tile animation. The +packet retains resources and copied draw values; it carries no map or sampler. +The [Rendering system](../engine-systems/rendering.md#integration) supplies the +pass/submission setup, and [Threads and Data Flow](../architecture/threads-data-flow.md) +explains publication to rendering. The contracts below cover Wang sites, +deterministic variants and direct-construction limits. + +### Selection contracts + The CPU-only `CE::Assets::Tilesets::Selection::select(rule, sampler, options)` overloads accept Wang, bitmask or variant definitions and return a `CE::Assets::Tilesets::Selection::Result`: either a `CE::Assets::Grids::CellIndex` or a `CE::Assets::Tilesets::Selection::Failure`. `MissingRule` means the derived signature/mask has no @@ -158,7 +238,7 @@ retained. Unused directions are not sampled. Read-only queries can run concurren when the supplied samplers and world support that access. `Known` samples carry an ID; zero means empty. `Outside` and `Unknown` samples ignore -their ID and have independent fallback policies: `Empty`, `CE::Rendering::Geometry::Center` or `Unresolved`. +their ID and have independent `Selection::Fallback` policies: `Empty`, `Center` or `Unresolved`. Defaults treat outside-world sites as empty and unknown sites as unresolved. A known Wang label absent from its terrain catalogue uses the unknown policy. A Wang center fallback requires `options.terrain` to be declared or zero. Bitmasks have no terrain diff --git a/docs/assets/index.md b/docs/assets/index.md new file mode 100644 index 00000000..0124f38c --- /dev/null +++ b/docs/assets/index.md @@ -0,0 +1,19 @@ +# Assets + +Load application content, author graphics manifests, and keep animation or tile +selection in game-owned state. The [Assets system](../engine-systems/assets.md) +introduces the public types; the sprite and animation tasks cover first use. + +```{toctree} +:maxdepth: 1 + +Load and publish assets +Author graphics manifests +Animate sprites and select tiles +Discover files and font candidates +Install optional artwork packages +``` + +For uploads during play, use the ownership transfers in +[Threads and Data Flow](../architecture/threads-data-flow.md#prepare-and-publish-a-replacement). +Font selection and shaped labels have a separate [Text](../text/index.md) path. diff --git a/docs/assets/text-layout.md b/docs/assets/text-layout.md index 3d093a5a..d7e1c4c6 100644 --- a/docs/assets/text-layout.md +++ b/docs/assets/text-layout.md @@ -1,5 +1,19 @@ # Unicode text layout and font resources +## Choose a text path + +| Content | Use | Working sequence | +| --- | --- | --- | +| A game-owned label or HUD message | Engine font selection, shaped layout and retained text generations | [Display and update text](../getting-started/how-to/update-text.md): prepare on CPU, upload on platform, adopt a ready result and submit it in a frame. | +| Text inside a native widget or RML document | The selected toolkit's authoring and font interfaces | The UI session owns layout/editing and publishes a retained scene; Engine supplies CPU font services underneath. | +| Existing ASCII or bitmap-atlas consumers | The legacy Font interfaces and their metrics | Keep their atlas, scale and bank compatibility contract during migration. | + +The [Text system](../engine-systems/text.md) identifies the public types, while +[Threads and Data Flow](../architecture/threads-data-flow.md#prepare-and-publish-a-replacement) +explains replacing a message without changing already published frames. + +## Supported rendering scope + The initial rendering scope is accented Latin (including English, French and German), Russian/Cyrillic, mixed paragraph direction and optional width-constrained wrapping. Glyph coverage and shaping are font/script concerns rather than a separate renderer @@ -22,6 +36,22 @@ generations. Its public stages have separate owners: that fallback model. Its `AsciiFont`/`BitmapFont` implementations and toolkit-owned text retain their separate roles. +Engine also owns the opaque +[`CE::Text::FontRasterizer`](../../projects/engine/include/cheryl/text/font-rasterizer.h) +CPU service for loading copied font bytes, properties, metrics, glyph pixels and +kerning. Copies retain and serialize native face state; returned data owns its +storage and needs no provider/context. TGUI and RmlUi adapters use this shared +implementation while retaining their own toolkit layout/atlas/effect logic. +FreeType rasterization, HarfBuzz shaping and ICU Unicode processing remain +distinct responsibilities, all implemented inside Engine. Toolkit color-glyph +handling does not establish color-emoji acceptance for this Engine layout path. + +Those native libraries use shared linkage. Linux uses distribution ICU libraries; +Windows 10 version 1903+ uses the operating system's ICU C API. Applications use +the same Cheryl text interfaces on both platforms; see +[dependency settings](../development/building.md#dependency-and-toolchain-settings) +for build and runtime requirements. + ## Font selection `CE::Text::FontCollection::load(selection)` creates an immutable CPU snapshot of font diff --git a/docs/audio/index.md b/docs/audio/index.md new file mode 100644 index 00000000..4d19d200 --- /dev/null +++ b/docs/audio/index.md @@ -0,0 +1,14 @@ +# Audio + +Prepare clips or streams, retain voices that need control, and close output after +its producers settle. Audio uses its own playback clock and application-owned +lifetime. The [Audio system](../engine-systems/audio.md) introduces the types. + +```{toctree} +:maxdepth: 1 + +Control playback and ownership <../runtime/audio> +``` + +The [miniaudio module](../modules/miniaudio.md#preparation-and-output) supplies the +concrete setup example and independent device/offline procedures. diff --git a/docs/development/architecture-validation.md b/docs/development/architecture-validation.md index cf9c1699..7144b829 100644 --- a/docs/development/architecture-validation.md +++ b/docs/development/architecture-validation.md @@ -15,9 +15,9 @@ separate from source completion. | Engine + Native GLFW + OpenGL | OpenGL consumer/probes, module implementation cases and selected owner/combined runner registration are correct. Native startup, presentation, retained-resource cleanup, input backpressure and concurrent shutdown require their explicit opt-ins. | | Standalone and supplied targets | Each module reuses one Engine; OpenGL reuses one Native GLFW. Dependency bootstrapping does not recursively enable root modules, tests or demo. Consumers link their actual owners and CTest discovers module-local cases. | | Crash bootstrap | An Engine-only consumer referencing no Engine entry point receives the automatic bootstrap object. Check the consumer's actual `NDEBUG` scope independently of exception/explicit traces or standalone Backward tests. | -| Startup support | Explicitly compile `cengine_startup` in Engine-only composition and `module_opengl_startup` through the native demo. Installed SDK checks compile both support headers independently. Plain Engine/OpenGL links remain free of CLI11 usage requirements; dedicated parser/factory regressions remain pending. | +| Startup module | Compile `module_startup` with OpenGL integration omitted for neutral composition and enabled for native composition. Check the generic and compatibility headers/factories plus selected-backend options. Plain Engine/OpenGL links remain free of CLI11 usage requirements. | | Worker support | Engine-only/JSON-only selection omits hwloc discovery/linkage. Controlled fixtures check owned snapshots, sparse/asymmetric topology, placement, fairness/caps and failure safety; native hwloc checks additionally require real eligible CPUs and binding support. | -| Installed SDK | Extract into a different prefix, reject producer-path leakage, compile public/Startup header probes and run consumers linked only to installed Cheryl libraries. Windows must use bundled dependency DLLs; source-tree consumers alone do not prove this path. | +| Installed SDK | Extract into another prefix, reject producer-path leakage, compile public/Startup/native-toolkit probes and link only installed owners. Check packaged and system shared runtimes with both Cheryl linkages. Static consumers also resolve embedded archives and native final-link requirements. Source-tree consumers alone do not prove relocation. | Controlled Engine tests establish runtime, publication and failure behavior against predictable dependencies. Implementing modules own checks of their actual behavior @@ -41,6 +41,28 @@ avoids C++ language-module scanning; Cheryl integration modules are libraries. build execution. [Runner selection](testing.md) explains focused and aggregate registration. +### Configure component combinations + +After [preparing an optional shared dependency prefix](building.md#dependency-and-toolchain-settings), +the configure-only matrix checks fresh Engine-only, neutral/native Startup, +Native GLFW, OpenGL, TGUI, RmlUi, audio, JSON-only/native Workers and supplied-Engine +compositions: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" + python3 .github/ci/configure_components.py \ + --vcpkg-root build/tools/vcpkg --installed-dir build/dependencies \ + --build-root build/component-validation +) +``` + +It selects Ninja, creates fresh `sdk-config-` directories and refuses to +overwrite existing ones. Use `--case NAME` to select a case and `--triplet` to +select another prepared target. The matrix performs no source builds; it checks +configuration and composition without establishing symbol linkage or execution. + ### Engine only A fresh Release configuration provides a neutral graph: @@ -97,7 +119,7 @@ probes while omitting optional topology helpers, modules, tests and SDK exports: -DCHERYL_BUILD_SDK=OFF -DCHERYL_BUILD_TESTS=OFF \ -DCHERYL_BUILD_CONSUMER_TESTS=ON cmake --build build/testing-engine-isolation --parallel --target \ - cengine_startup consumer-cengine + module_startup consumer-cengine ./build/testing-engine-isolation/cheryl-consumer ninja -C build/testing-engine-isolation -t commands consumer-cengine ) @@ -143,7 +165,7 @@ caller's working directory: -DCHERYL_BUILD_ALL_TESTS=OFF -DCHERYL_BUILD_ACCEPTANCE_TESTS=ON \ -DCHERYL_BUILD_DEMO=OFF cmake --build build/testing-engine --parallel "$(nproc)" --target \ - cengine_startup tests-engine consumer-cengine + module_startup tests-engine consumer-cengine ./build/testing-engine/cheryl-consumer ctest --test-dir build/testing-engine --parallel "$(nproc)" --output-on-failure \ --no-tests=error \ @@ -219,7 +241,7 @@ Correct masks/progress do not establish cache residency or a workload speedup. ### Installed SDK consumers -The [CI producer profile](building.md#github-ci-and-sdk-artifacts) configures Ninja +The [CI producer profile](ci.md) configures Ninja with the selected modules/helpers, explicit owner targets and `CHERYL_BUILD_SDK=ON`. Use a clean pinned checkout and the producer's dependencies/toolchain. Supply the font fixtures for complete allocation coverage; missing CFF coverage is reported @@ -238,14 +260,17 @@ separately. On Linux, a local equivalent is: Reuse `build/ci` only when its recorded source/toolchain/configuration/features match; omit the producer step when the matching build/checks are already accepted. The -consumer directory and its sibling `sdk-consumer-relocated-sdk` must be fresh. -The verifier checks SHA256, archive contents, producer paths and seven selected -consumer programs plus first-include and Startup probes. It uses checkout test -fixtures but imports installed Cheryl libraries, rebuilding none of their source. -Windows CI uses the same steps under its MSVC Developer PowerShell with the -manifest's vcpkg toolchain and DLL prefix. The selected hosted Ubuntu and Windows -producer/relocated-consumer paths are accepted within the -[CI coverage limits](building.md#github-ci-and-sdk-artifacts). The first main-push +consumer directory and its sibling `sdk-consumer-relocated-sdk` must be fresh; +the verifier also creates sibling hermetic and standalone-source directories. +It checks SHA256, archive contents, producer paths, independent consumers and +first-include/Startup/native-toolkit probes. Its hermetic composition clears +ambient package discovery and builds a standalone teaching application against +the relocated SDK. Test fixtures come from the checkout; Cheryl implementation +sources are not rebuilt. A JUnit report records verifier results. +The Linux producer uses distribution development packages. Windows CI uses the +same steps under its MSVC Developer PowerShell with the prepared shared dependency +toolchain. Platform acceptance remains subject to the +[CI coverage limits](ci.md). The first main-push artifact collection remains [TR20](../testing-requests.md#tr20-automated-main-push-sdk-collection). This reusable procedure does not assert acceptance of unexecuted new cases. diff --git a/docs/development/building.md b/docs/development/building.md index f187b409..9c6a6ee3 100644 --- a/docs/development/building.md +++ b/docs/development/building.md @@ -65,7 +65,11 @@ dependency-source settings may appear only when their component is selected. | Option | Default | Effect | | --- | --- | --- | +| `CHERYL_BUILD_STARTUP` | `ON` | Selects the public application-bootstrap module. | +| `CHERYL_STARTUP_OPENGL` | Follows selected OpenGL availability | Includes private configured OpenGL integration in Startup; does not create another startup binary. | +| `CHERYL_BUILD_WORKERS` | `ON` | Makes the optional worker configuration/topology module available when a feature is selected. | | `CHERYL_BUILD_NATIVE_GLFW` | `ON` | Selects GLFW display/windows and optional input. | +| `CHERYL_BUILD_NATIVE_SDL3` | `OFF` | Adds the source-only [SDL3 scaffold](modules.md#sdl3-scaffold); no runtime adapter or SDK component. | | `CHERYL_BUILD_DEBUG_TERMINAL_LINUX` | Single-configuration Linux GNU/Clang with `CMAKE_BUILD_TYPE=Debug`: `ON`; otherwise: `OFF` | Includes the optional Linux output terminal in consuming applications/test runners. Explicit `ON` includes it in every configuration built from that directory. | | `CHERYL_DEBUG_TERMINAL_EMULATOR` | `auto` | Linux module launcher selection: `auto`, `konsole` or `xterm`. | | `CHERYL_BUILD_OPENGL` | `ON` | Selects the complete OpenGL backend; requires Native GLFW. | @@ -74,6 +78,7 @@ dependency-source settings may appear only when their component is selected. | `CHERYL_BUILD_AUDIO_MINIAUDIO` | `ON` | Selects miniaudio decoding, output and streaming. | | `CHERYL_BUILD_DEMO` | `OFF` | Adds `demo` with Native GLFW, OpenGL and native input selected. | | `CHERYL_BUILD_FIRST_GAME` | `OFF` | Adds the maintained `first-game` walkthrough application with Native GLFW, OpenGL and native input selected. | +| `CHERYL_BUILD_TUTORIAL_GAME` | `OFF` | Adds `tutorial-game`, combining Player/Enemy/HUD and six How-to tasks; requires Startup/OpenGL, native input and TGUI. | | `CHERYL_BUILD_DOCS` | `ON` | Adds the optional `docs-tools`, `docs` and `docs-clean` targets; tools are selected when these targets run. | | `CHERYL_BUILD_TESTS` | `OFF` | Adds focused tests and explicitly buildable acceptance/aggregate runners. | | `CHERYL_BUILD_ALL_TESTS` | `OFF` | Includes aggregates in the default build and CTest; requires tests. | @@ -82,7 +87,7 @@ dependency-source settings may appear only when their component is selected. | `CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC` | `ON` | Selects optional runtime topology discovery and thread binding through hwloc. | | `CHERYL_BUILD_WORKER_TOPOLOGY_JSON` | `ON` | Selects the versioned worker settings loader independently of hwloc. | | `CHERYL_BUILD_SDK` | `ON` | Adds the `cheryl-sdk` build target and `CherylSDK` install/export component for selected libraries, headers and dependencies. | -| `CHERYL_BUILD_SHARED_LIBS` | `ON` | Builds Cheryl Engine, compiled modules and support as shared libraries, with matching owned source dependencies. | +| `CHERYL_BUILD_SHARED_LIBS` | `ON` | Builds Engine, Startup and selected compiled modules as separate shared libraries; external dependency linkage follows the per-library policy below. | | `CHERYL_NATIVE_INPUT` | `ON` | Includes Gainput-backed input in Native GLFW. | | `CHERYL_NATIVE_NULL_PLATFORM` | `OFF` | Configures an owned GLFW without X11/Wayland. Disable native input to omit Gainput/X11. | | `CHERYL_LOG_PROFILE` | `auto` | Selects `developer`, `support` or `release`; `auto` follows the build configuration. | @@ -111,7 +116,9 @@ substitute `RelWithDebInfo`/`build/support` or `Release`/`build/release` as need ) ``` -Non-MSVC Debug enables AddressSanitizer and UndefinedBehaviorSanitizer. Automatic +This Linux example uses installed shared development packages. Windows uses the +prepared dependency arguments below. Non-MSVC Debug enables AddressSanitizer and +UndefinedBehaviorSanitizer. Automatic logging selects developer in Debug, support in RelWithDebInfo and release otherwise. All translation units sharing Cheryl headers must use the same compiled logging definitions; runtime levels cannot restore stripped severities. @@ -125,13 +132,15 @@ Target and artifact declarations are centralized in ### Library linkage -Cheryl Engine, modules and support default to shared libraries. Set +Cheryl Engine, Startup and compiled modules default to separate shared libraries. Set `-DCHERYL_BUILD_SHARED_LIBS=OFF` during configuration for static libraries; the same public CMake targets work with either selection. `BUILD_SHARED_LIBS` alone -does not select Cheryl's linkage. Keep static/shared builds and SDK prefixes separate. Supplied dependency -targets retain their settings and must satisfy the selected shared-library requirements. -The Linux terminal's `main` wrapper remains static; its startup support follows -the selected Cheryl linkage. +does not select Cheryl's linkage. Keep static/shared builds and SDK prefixes separate. +External libraries keep the same linkage in ordinary builds and SDK builds, +including when Cheryl itself is static; see the per-library policy below. +Private support contributes to its owner rather than creating a public runtime +binary. The Linux terminal's `main` wrapper and argument/startup attachment remain +static; its viewer is an executable. Artifact names use the component versions in [CherylVersions.cmake](../../cmake/CherylVersions.cmake). For Engine 4.0.0: @@ -155,6 +164,7 @@ its text dependencies: set -e cd "$(git rev-parse --show-toplevel)" cmake -S . -B build/engine-only -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCHERYL_BUILD_STARTUP=OFF -DCHERYL_STARTUP_OPENGL=OFF -DCHERYL_BUILD_WORKERS=OFF \ -DCHERYL_BUILD_NATIVE_GLFW=OFF -DCHERYL_BUILD_OPENGL=OFF \ -DCHERYL_BUILD_DEBUG_TERMINAL_LINUX=OFF \ -DCHERYL_BUILD_UI_TGUI=OFF -DCHERYL_BUILD_UI_RMLUI=OFF \ @@ -168,16 +178,79 @@ its text dependencies: ## Dependency and toolchain settings +Dependency linkage is selected per library, independently of SDK installation and +`CHERYL_BUILD_SHARED_LIBS`. Supplied targets retain their host settings and must +satisfy the same policy; incompatible targets fail configuration. + +| Library | Linkage | Ownership and deployment | +| --- | --- | --- | +| FreeType, HarfBuzz | Shared | Private Engine implementation. Use Linux development packages or prepared Windows DLLs/import libraries. | +| ICU | System shared | Linux uses the distribution's `uc`, `i18n` and `data` libraries. Windows uses the OS C API from `icu.dll`; Windows 10 version 1903 or later and a Windows SDK containing `icu.h`/`icu.lib` are required. | +| GLFW | Shared | Native GLFW owns its integration; the owned source build supplies one shared GLFW runtime. | +| HIDAPI | Shared when HID is enabled | Reuse a compatible package or build the pinned source. Gainput remains embedded in Native GLFW. | +| TGUI, RmlUi Core | Shared | Reviewed custom builds expose native authoring APIs. Private callbacks reach Engine services through their Cheryl adapters; the toolkit libraries do not link Engine. | +| hwloc 2.5+ (2.x) | Shared when selected | Private Workers implementation; disable `CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC` to omit discovery and linkage. | +| OpenGL and native platform libraries | System shared | Use the host's graphics driver and OS libraries. Generated GLAD is embedded in the Cheryl OpenGL owner. | +| spdlog, Gainput, miniaudio, GLAD, stb | Embedded | Each compiled implementation has one Cheryl owner; shared owners require position-independent archive inputs. | +| Backward support | Embedded headers/bootstrap, system shared diagnostics | Engine owns crash initialization. Selected Linux libdw/libelf and compression libraries or Windows dbghelp/psapi remain platform dependencies. | +| GLM, CTTI, CLI11 | Headers/templates | Public target usage requirements remain explicit; CLI11 belongs to Startup. JSON stays private to its consuming implementations. | + +Microsoft documents the [Windows ICU C API and combined 1903+ runtime](https://learn.microsoft.com/en-us/windows/win32/intl/international-components-for-unicode--icu-). +Cheryl uses that C API directly; the OS supplies `icu.dll` and the Windows SDK +supplies `icu.h` and `icu.lib`. + +Linux development can configure directly against installed shared development +packages; no vcpkg toolchain is required. Windows does not provide FreeType, +HarfBuzz, GLFW, HIDAPI, UI toolkits or hwloc as OS libraries. Prepare the external +font libraries and optional hwloc/HIDAPI with the helper below. The same helper +also provides an optional pinned Linux dependency prefix: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" + python3 tools/dependencies/prepare.py --native \ + --vcpkg-root "$PWD/build/tools/vcpkg" --installed-dir "$PWD/build/dependencies" +) +``` + +Use `--without-hwloc` when omitting that feature. Omit `--native` when the selected +native input does not need HIDAPI. `--jobs` is optional; user-run preparation +otherwise uses normal available parallelism. The helper checks the pinned vcpkg +revision without rewriting a supplied checkout and records the dependency +identity in its installed root. It prints the selected shared prefix and toolchain; +use those with CMake. This optional prefix example selects Linux x64: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" + cmake -S . -B build/release -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE="$PWD/build/tools/vcpkg/scripts/buildsystems/vcpkg.cmake" \ + -DVCPKG_INSTALLED_DIR="$PWD/build/dependencies" -DVCPKG_TARGET_TRIPLET=x64-linux-dynamic \ + -DVCPKG_MANIFEST_DIR="$PWD/.github/ci" -DVCPKG_MANIFEST_INSTALL=OFF \ + -DCMAKE_PROJECT_INCLUDE="$PWD/tools/dependencies/prepared.cmake" +) +``` + +Run that configuration after preparation. The toolchain +resolves the prepared shared font/layout packages; +`prepared.cmake` supplies prepared hwloc and Windows HID targets when selected. +Existing compatible targets take precedence. For Windows x64, replace the triplet +with `x64-windows` and add +`-DCMAKE_MSVC_RUNTIME_LIBRARY='MultiThreaded$<$:Debug>DLL'`. +Use a matching prepared target triplet for other architectures and cross builds. + | Setting | Purpose | | --- | --- | | `GLFW_BUILD_WAYLAND=OFF` | Uses bundled GLFW's X11 backend on Linux without Wayland development tools. | -| `GAINPUT_ENABLE_HID=OFF` | Omits Gainput's HID support and hidapi fetch. Use the [current Linux joystick path](../../projects/modules/platform/native-glfw/README.md#controller-backend-limits) while HID integration remains unresolved. | +| `GAINPUT_ENABLE_HID=OFF` | Omits Gainput's HID support and HIDAPI discovery/fetch on Linux; this is the Linux default. Windows forces that backend and uses shared HIDAPI. Use the [current Linux joystick path](../../projects/modules/platform/native-glfw/README.md#controller-backend-limits) while HID integration remains unresolved. | | `Python_EXECUTABLE=/path/to/python` | Selects GLAD's interpreter; Jinja2 must be installed in it. | | `CMAKE_POLICY_VERSION_MINIMUM=3.5` | Allows pinned legacy dependency files to configure with CMake 4 when needed. | -| `CHERYL_REPOSITORY_ROOT=/path/to/cheryl-engine` | Supplies shared helpers and Engine bootstrapping for standalone modules/consumers. Root configuration sets it automatically. | +| `CHERYL_REPOSITORY_ROOT=/path/to/cheryl-engine` | Explicitly selects the checkout providing shared helpers and Engine bootstrapping for standalone modules/consumers; entry points otherwise locate their enclosing checkout. | | `CHERYL_NATIVE_GLFW_SOURCE=/path/to/native-glfw` | Supplies Native GLFW to standalone OpenGL when its target is absent. | -| `CHERYL_TGUI_SOURCE`, `TGUI_DIR` | Selects a TGUI 1.13.0 source tree or exact package with custom/FreeType support. | -| `CHERYL_RMLUI_SOURCE`, `RmlUi_DIR` | Selects a RmlUi 6.3 source tree or exact package with stock FreeType and the required [placeholder correction](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract). | +| `CHERYL_TGUI_SOURCE`, `TGUI_DIR` | Selects a TGUI 1.13.0 source tree or verified shared package with custom Engine font/image services and the private decoder ABI. | +| `CHERYL_RMLUI_SOURCE`, `RmlUi_DIR` | Selects a RmlUi 6.3 source tree or verified shared package using Engine font services, the private rasterizer ABI and the required [placeholder correction](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract). | | `CHERYL_MINIAUDIO_SOURCE` | Overrides pinned miniaudio source; a supplied miniaudio target takes precedence. | | `CHERYL_RMLUI_TEST_FONT=/path/to/font.ttf` | Supplies a real font fixture when the selected SDK's sample font is unavailable. | | `CHERYL_HWLOC_TARGET=existing_target` | Supplies a compatible hwloc 2.5+ (2.x) target when worker discovery/binding is selected; otherwise discovery checks supported config targets and pkg-config. | @@ -185,14 +258,15 @@ its text dependencies: Modules reuse compatible supplied dependency targets where supported. Their owner guides document selection order and feature requirements. Owned source dependencies are pinned in [.gitmodules](../../.gitmodules); initialize the recorded revisions. -The first HID-enabled configuration fetches hidapi 0.15.0 and needs network access -or a prepared FetchContent cache. UI dependency selection performs no download. - -Engine configuration registers `Cheryl::Startup`, reusing `CLI11::CLI11` or adding -the pinned `extern/cli11` source. OpenGL registers `Cheryl::OpenGL::Startup`. -These support libraries are excluded from the default build unless a consumer -links them or names their build targets; the demo links OpenGL startup. Plain -Engine/OpenGL targets do not acquire CLI11 usage requirements. See +HID-enabled selection first reuses a target or installed package; Linux also +checks pkg-config, preferring hidraw unless configured otherwise. If none is +available, configuration fetches HIDAPI 0.15.0 and needs network access or a +prepared FetchContent cache. UI dependency selection performs no download. + +The selected Startup module reuses `CLI11::CLI11` or adds pinned `extern/cli11`. +Its private OpenGL integration is selected by `CHERYL_STARTUP_OPENGL`; +`Cheryl::OpenGL::Startup` remains a compatibility interface over the same binary. +Plain Engine/OpenGL targets do not acquire CLI11 usage requirements. See [application startup](consuming-engine.md#command-line-startup). The terminal module adds a separate optional startup target and linker entry hook; @@ -220,13 +294,14 @@ Package names vary by operating system. | --- | --- | --- | | Threads | Engine | Discovered through `Threads::Threads`. | | Python 3.11+ | Documentation targets (enabled by default) | Required during configuration; Doxygen/Sphinx are selected when documentation targets run. | -| hwloc 2.5+ (2.x) | WorkerTopology::Hwloc (selected by default) | Runtime CPU/core/cache discovery and calling-thread binding; omitted configurations do not discover/download/link it. | -| FreeType, HarfBuzz, ICU uc/i18n | Engine | Font inspection/rasterization, shaping, bidi and boundaries; required with UI disabled too. | +| Shared hwloc 2.5+ (2.x) | Workers hwloc feature (selected by default) | Runtime CPU/core/cache discovery and binding; omitted configurations do not discover or link it. | +| Shared FreeType and HarfBuzz | Engine | Font inspection/rasterization and shaping; required with UI disabled too. | +| ICU | Engine | Linux shared `uc`/`i18n`/`data` development packages; Windows 10 1903+ supplies the runtime and the Windows SDK supplies the C API headers/import library. | | X11 and extensions | GLFW X11 / native Linux input | GLFW checks XRandR, Xinerama, XKB, Xcursor, XInput and X Shape headers. Gainput input also needs X11. | | Wayland, xkbcommon, `wayland-scanner` | Bundled GLFW Wayland | Linux GLFW enables X11 and Wayland by default. Disable Wayland for an X11-only build. | | OpenGL | OpenGL module | Headers/link libraries; native execution also needs a usable driver/display. | | Python and Jinja2 | GLAD generation | Jinja2 must be available in CMake's selected interpreter. | -| FreeType | TGUI / RmlUi | Each toolkit uses its own font service. | +| Engine font/image services | TGUI / RmlUi | Toolkit adapters share Engine implementation instead of embedding another font/decoder copy. | | libudev / libusb / Iconv | Linux hidapi | Requirements depend on selected HID backends; SDK consumers need the selected backend's native libraries too. | | Konsole or xterm | Linux output terminal at runtime | A desktop is needed for display; neither is an Engine build dependency. | | libdw, libbfd, or libdwarf/libelf | Backward, optional | Improve trace source/symbol resolution. | @@ -242,11 +317,12 @@ Integration SDKs belong to their modules; GoogleTest is test-only. See | --- | --- | | Selected dependency directory is empty | Initialize the pinned submodules. | | A selected target is missing in CLion | Change the cached option and reload CMake. | +| An old profile requires static FreeType/HarfBuzz | Reload with the current dependency policy and use a fresh build directory for a profile retaining a static toolchain/triplet. Ordinary Linux shared packages are supported. | | Python cannot import `jinja2` | Install it in the selected interpreter or set `Python_EXECUTABLE`. | | GLFW cannot find Wayland tools/libraries | Install the requirements or set `GLFW_BUILD_WAYLAND=OFF`. | | CMake 4 rejects an old dependency policy | Set `CMAKE_POLICY_VERSION_MINIMUM=3.5`. | | Supplied RmlUi Core lacks the placeholder-correction declaration | Use Cheryl-owned source or declare the correction on the verified dependency's Core target according to the [dependency contract](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract). | -| Standard C++ headers cannot find `CE::Math.h` or `stdlib.h` | Inspect stale compiler discovery before editing includes; see [recovery](consuming-engine.md#standard-headers-in-an-existing-build). | +| Standard C++ headers cannot find `math.h` or `stdlib.h` | Inspect stale compiler discovery before editing includes; see [recovery](consuming-engine.md#standard-headers-in-an-existing-build). | Successful configuration/build does not establish native platform coverage. The [test guide](testing.md) and [architecture validation](architecture-validation.md) @@ -258,10 +334,11 @@ the same targets without graphics or native development dependencies. ## Optional worker support -`CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC=ON` adds `cengine_worker_topology_hwloc` -(`Cheryl::WorkerTopology::Hwloc`); JSON selection adds `cengine_worker_topology_json` -(`Cheryl::WorkerTopology::Json`). Both options default to `ON`; a plain Engine -consumer can disable both because it needs neither support library. Selecting both +`CHERYL_BUILD_WORKERS=ON` with a selected feature adds `module_workers` +(`Cheryl::Workers`). `CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC` and +`CHERYL_BUILD_WORKER_TOPOLOGY_JSON` independently select its capabilities; both +default to `ON`. The capability-specific legacy target names refer to that same +implementation. A plain Engine consumer can omit Workers. Selecting both features also registers the explicitly buildable `example_worker_topology` shared-pool composition example. The [worker guide](../runtime/worker-execution.md) owns provider lifetime, placement policies, capacity and configuration semantics. @@ -277,16 +354,22 @@ CPU vendor is not a CMake selection. ## Development SDK builds -SDK export is enabled by default and requires Cheryl-owned builds of the pinned source -dependencies; imported dependency targets are rejected by this packaging path, -except an imported hidapi package that remains external to the SDK. -Ordinary source composition can still reuse supplied targets. Use a clean checkout +SDK export is enabled by default and packages the same libraries used in ordinary +development. Select the modules and optional features to distribute with the +normal build options; omitting hwloc, for example, needs no separate SDK mode. +Embedded dependencies remain inside shared Cheryl owners; static Cheryl SDKs +retain their embedded archive closure for final linking. Owned shared runtimes +such as GLFW and the reviewed UI toolkits are installed with either SDK linkage. +Windows also packages selected external DLLs/import libraries. Linux distribution +libraries remain external requirements. `CHERYL_SDK_NATIVE_PREFIX` can supply +dependency license notices; the vcpkg toolchain derives it automatically. Use a clean checkout with submodules at their recorded revisions for reproducible archives. The packager records the source commit and verifies recursive submodule HEADs against its gitlinks. Use a single-configuration Ninja build and select the modules/helpers to distribute. -On Linux, this builds the default modules and worker support, plus common Startup -support, without building the demo or tests: +With [shared development packages](#dependency-and-toolchain-settings), this Linux +configuration builds the default modules, including Startup and Workers, +without building the demo or tests: ```sh ( @@ -303,10 +386,10 @@ support, without building the demo or tests: ``` `cheryl-sdk` builds the selected libraries and their dependencies. It does not -copy headers, install a directory or create an archive. With native input and HID -enabled, SDK export includes the owned hidapi backend selected by Gainput, its -headers and licenses. Linux consumers still need its selected libudev/libusb and -Iconv dependencies; the package config discovers them. This packaging support does +copy headers, install a directory or create an archive. HID-enabled native input +embeds Gainput and links shared HIDAPI. Selected platform system libraries, +including Linux libudev/libusb when required by that backend, remain external. +Static Cheryl SDKs carry the corresponding final-link requirements. This packaging support does not change the [Linux controller limitations](../../projects/modules/platform/native-glfw/README.md#controller-backend-limits). To collect the libraries and redistributable headers into a local SDK prefix after that build: @@ -322,7 +405,7 @@ that build: The prefix contains `include/`, `lib/`, `lib/cmake/Cheryl/` and `share/licenses/` with the configuration above. Windows may also install runtime libraries under `bin/`. The install command does not compile missing libraries; run the build step -first. Consumers use `find_package(Cheryl CONFIG REQUIRED)` with this prefix in +first. Consumers use `find_package(Cheryl 4 CONFIG REQUIRED)` with this prefix in `CMAKE_PREFIX_PATH`; see [SDK consumption](consuming-engine.md#prebuilt-development-sdks). For a complete distributable archive, use Python 3.12 or newer: @@ -338,8 +421,9 @@ For a complete distributable archive, use Python 3.12 or newer: `package.py` performs its own temporary install of only the `CherylSDK` component, creates a Linux `.tar.gz` or Windows `.zip` under `build/packages/`, and writes `SHA256SUMS` plus `SDK-MANIFEST.json` there. You can skip the direct install command -when you only need an archive. Windows archive production additionally copies its -native dependency prefix; use `package.py` for the complete Windows bundle. +when you only need an archive. SDK installation records each shared runtime and +whether the SDK supplies it or the platform must supply it. A static Cheryl SDK +still needs the shared native libraries selected by the same per-library policy. Archive names include the Cheryl version from [CherylVersions.cmake](../../cmake/CherylVersions.cmake), platform/distribution, architecture, compiler/version, configuration and commit ID. @@ -347,174 +431,18 @@ They also include `static` or `shared`; `SDK-MANIFEST.json` records that linkage enabled component versions and the exact Engine artifact paths. Linux archives preserve relative SONAME symlinks within the SDK; broken or escaping links are rejected. -Windows CI builds through the pinned vcpkg manifest in [.github/ci](../../.github/ci/vcpkg.json) -with the `x64-windows` triplet. Packaging copies release headers, import libraries, -DLLs, CMake files and licenses from `build/ci/vcpkg_installed/x64-windows` into -`dependencies/`; `--native-prefix` can select another prepared prefix. Debug -dependency trees, tools and applications are omitted. Linux retains system native -dependencies. Neither package contains the demo or C/C++ implementation translation +CI uses Linux distribution development packages and prepares Windows shared native +dependencies with `x64-windows` and the DLL CRT. `CHERYL_SDK_NATIVE_PREFIX` can select +a prepared prefix for license notices during configuration. Debug dependency trees, tools and applications +are omitted. Platform +system libraries remain external. Neither package contains the demo or C/C++ implementation translation units; public and inline/template headers remain included. The selected libraries are already compiled. See [SDK consumption](consuming-engine.md#prebuilt-development-sdks) for ABI, logging and runtime-library requirements. ## GitHub CI and SDK artifacts -[ci.yml](../../.github/workflows/ci.yml) validates pushes to `develop`/`main`, pull -requests targeting either branch and manual dispatches. Each job checks out recorded -recursive submodules. Actions use commit pins; Windows uses the manifest's pinned -vcpkg baseline. Builds use Ninja and available CPU parallelism. - -| Job | Host/compiler/configuration | Selection | -| --- | --- | --- | -| `linux-gcc-hwloc` | Ubuntu 24.04 x64, GCC 14 Release | Headless Engine, hwloc and JSON. | -| `linux-gcc-neutral` | Ubuntu 24.04 x64, GCC 14 Release | Headless Engine and JSON, hwloc omitted. | -| `linux-clang-debug` | Ubuntu 24.04 x64, Clang 18 Debug | Headless Engine, hwloc/JSON, ASan/UBSan. | -| `windows-msvc-hwloc` | Windows 2025 x64, MSVC RelWithDebInfo | Headless Engine and hwloc/JSON with vcpkg native libraries. | -| `linux-modules` | Ubuntu 24.04 x64, GCC 14 Release | Native GLFW/input, OpenGL, TGUI, RmlUi, miniaudio, hwloc/JSON and SDK. | -| `windows-modules` | Windows 2025 x64, MSVC RelWithDebInfo | The same selected modules/helpers and SDK, with bundled native dependencies. | -| `linux-modules-shared` | Ubuntu 24.04 x64, GCC 14 Release | The modules/SDK composition with shared Cheryl libraries and owned dependencies. | -| `windows-modules-shared` | Windows 2025 x64, MSVC RelWithDebInfo | The shared modules/SDK composition, including DLL runtime discovery. | - -[run.py](../../.github/ci/run.py) configures explicit owners/options/build targets, -runs each focused owner prefix once and executes independent consumers. Modules -jobs compile the demo for composition coverage but never launch or package it. -Linux jobs provide TrueType/CFF allocation fixtures. Missing fixtures and -platform-specific skips are reported as unaccepted coverage. Native OpenGL, native -hwloc binding acceptance, actual audio devices, desktop/controller QA and the -Debug terminal remain separate from this focused CI selection. - -Modules jobs package and verify an extracted SDK on every validation run. -[verify_sdk.py](../../.github/ci/verify_sdk.py) checks the checksum and absence of -producer paths, then builds independent consumers/header probes using only installed -Cheryl libraries. Test fixture sources/fonts come from the checkout; Cheryl and -module implementation sources are not rebuilt. Use a fresh consumer build/extraction -directory for each verification. See -[repeating SDK checks](architecture-validation.md#installed-sdk-consumers). - -Successful main-push modules jobs retain SDK candidates for three days. After all -headless and modules jobs succeed, the collection job verifies checksums and uploads -`cheryl-development-sdks-` for 90 days. Test/configuration reports are retained -for 14 days. These are workflow artifacts; the workflow creates no GitHub release, -uploads no release assets and publishes no demo package. Release publication is a -separate future decision. - -The first main-push artifact collection remains -[TR20](../testing-requests.md#tr20-automated-main-push-sdk-collection); collection is -intentionally skipped on PR, `develop` and manual runs. - -### Actions cache retention - -Compiler and dependency caches speed up subsequent CI runs. They are separate -from the SDK and diagnostic artifacts above, and from each other: - -| Cache | Key family | Contents and ownership | -| --- | --- | --- | -| Compiler results | `ccache-v2` | Main, develop and PR layers, described below. | -| X: build dependencies | `build-deps-v2` | Separate main/develop submodule Git data and Windows vcpkg package archives/downloads. PRs read their base branch's X. | -| Y: Actions environment | `actions-env-cache-v2` | Shared tools, Linux package downloads, Python wheels/downloads and prepared documentation tools, published by main. | - -Each compiler matrix configuration has separate ccache layers: - -| Build | Readable layers | Writable layer saved after building | -| --- | --- | --- | -| `main` | Main baseline A | A | -| `develop` | A + develop additions B | B | -| PR targeting `develop` | A + B + PR additions C | C | -| PR targeting `main` | A + C | C | - -[restore-compiler-cache](../../.github/actions/restore-compiler-cache/action.yml) -restores the available layers into separate directories. -[compiler_cache.py](../../.github/ci/compiler_cache.py) configures ccache to check -the current layer first, followed by its parents, and write compilation results -only into the current layer. Parent hits stay in the parent layers. Statistics -live outside the archived data. Before saving, the helper removes compiled -results already available under the same cache key in a parent. It preserves -distinct lookup metadata, then trims the writable layer to 1 GiB. -An unchanged layer is not uploaded again. A layer that becomes empty replaces its -previous snapshot with a small empty-layer marker, allowing cleanup to remove -the older duplicated data. - -This hierarchy requires `main` to be GitHub's default branch. A PR can read its -own cache, its base branch's cache and the default branch's cache; a PR targeting -`main` cannot read `develop`'s cache. Manual feature-branch builds use their own -writable layer and the main baseline. These are -[GitHub cache scope restrictions](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache). -Missing parents cause normal cache misses until their builds populate them. -The `ccache-v2` keys distinguish this file-backend layout from the previous -ordinary local-cache archives; the first build of each configuration seeds the -new format. Compiler layers and Windows binary dependency snapshots use new -run-specific keys because saved GitHub caches are immutable. - -Cache X's source key hashes `.gitmodules` and the recorded submodule commits. -Updating a submodule selects a new source snapshot automatically; an older X can -seed Git objects before checkout fetches the requested commits. The binary -component follows the vcpkg manifest and runner image, with vcpkg validating each -package's binary compatibility when using a fallback snapshot. Vendored libraries -remain ordinary CMake targets and reuse their compiler results through ccache. -Only main/develop push and manual runs publish X; PRs fetch or build missing -dependencies locally. - -To update Y, change package/tool selections in -[environment.json](../../.github/ci/environment.json), generator packages in -[requirements-modules.txt](../../.github/ci/requirements-modules.txt), or the -documentation tool manifests. Bump `cache_version` from `2` to `3` when revising -the Actions environment or forcing a refresh; this changes the family to -`actions-env-cache-v3`. Environment inputs are also hashed so package changes -cannot silently reuse a mismatched key. Y has separate immutable components for -each OS/architecture, runner image and environment composition. Resolved Linux -package versions, Python ABI and tool manifests further identify their payloads; -submodule commits and run IDs do not affect Y. - -Every branch restores Y, while only trusted main push/manual runs publish it. -Develop and PR changes to the environment install normally on a cache miss; -main publishes the updated Y after those changes reach it. Package installers -still run on each fresh runner; cached downloads avoid fetching the same packages -again. Documentation tooling is prepared and cached before document generation, -so a later documentation failure does not discard the prepared environment. - -[cache-maintenance.yml](../../.github/workflows/cache-maintenance.yml) runs after -the build or documentation workflow finishes and daily at 04:23 UTC. It removes -caches belonging to closed pull requests, keeps the newest mutable compiler and -dependency snapshot per branch, configuration and cache version, and trims older caches -toward an 8 GiB repository budget. Active workflow refs and caches created or -accessed within the last 15 minutes are protected. After removing redundant -snapshots, budget eviction removes ordinary old caches first, then the latest -open-PR compiler/dependency snapshots, then develop snapshots, and finally main -snapshots. Known default-branch tooling caches have the same priority as develop -layers. Each tier uses oldest access first. The newest X source snapshot per -branch and runner receives that branch's priority; older submodule pins remain -ordinary eviction candidates. Old-format compiler snapshots lose their branch -priority once the same branch and configuration has a new-format replacement. -These priorities preserve useful branch caches where space allows; the budget -can still require evicting a current layer. The budget is a target: concurrent -uploads and protected caches can temporarily keep usage above it. -Cleanup deletes cached build inputs, without deleting SDK artifacts or changing -build results; an evicted cache is regenerated when next needed. - -To inspect or apply cleanup, open **Actions → Maintain Actions caches → Run -workflow** after this workflow has reached the repository's default branch. -Leave **dry_run** enabled to preview cache IDs, keys, refs, sizes and deletion -reasons. Optionally enter a closed PR number in **closed_pr** to restrict the -operation to that PR's caches. Review the run's report, then dispatch again with -**dry_run** disabled to apply the current plan. Automatic runs apply cleanup; -they execute the default branch's script without loading PR code, artifacts or -cached executables. - -GitHub also [expires unused caches and evicts old entries at its storage -limit](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#usage-limits-and-eviction-policy). -The maintenance policy removes redundant snapshots before that limit causes -frequent replacement of useful caches. - -### Hosted Windows coverage limits - -Both Windows profiles leave these focused cases unaccepted: - -| Cases | Missing prerequisite and next action | -| --- | --- | -| `font_bake.real_nested_allocation_scope`, `font_bake.real_cff_alloc_failure` | The workflow supplies the bundled TrueType face but no CFF OpenType fixture on Windows. Supply `CHERYL_STB_ALLOCATION_CFF` with a suitable face, retain the TrueType fixture for nested scopes, and rerun these cases on hosted Windows. | -| `logging.config_reopen` | The test needs a temporary directory on the checkout's filesystem root so it can use a relative log path. Configure the hosted Windows temporary directory on that drive, then rerun the case. | - -The [font fixture requirements](architecture-validation.md#native-and-fixture-checks) -define suitable faces. A green job does not accept these skipped cases; successful -local MSVC/Wine checks do not replace the missing hosted execution. These limits -are separate from SDK collection and desktop/GPU/device acceptance. +[CI and SDK artifacts](ci.md) describes the hosted build matrix, installed-consumer +checks, artifact collection, cache maintenance and Windows coverage limits. Use +this build guide for local configuration and SDK production; use the CI guide to +operate or investigate those workflows. diff --git a/docs/development/ci.md b/docs/development/ci.md new file mode 100644 index 00000000..93aa90f3 --- /dev/null +++ b/docs/development/ci.md @@ -0,0 +1,167 @@ +# CI and SDK artifacts + +Use the hosted workflows to validate selected compositions, collect prebuilt SDKs +and maintain build caches. [Building Cheryl](building.md) owns local configuration +and SDK production; [test selection](testing.md) owns runner and fixture choices. + +## GitHub CI and SDK artifacts + +[ci.yml](../../.github/workflows/ci.yml) validates pushes to `develop`/`main`, pull +requests targeting either branch and manual dispatches. Each job checks out recorded +recursive submodules. Actions use commit pins; Windows uses the manifest's pinned +vcpkg baseline. Builds use Ninja and available CPU parallelism. + +| Job | Host/compiler/configuration | Selection | +| --- | --- | --- | +| `linux-gcc-hwloc` | Ubuntu 24.04 x64, GCC 14 Release | Headless Engine, hwloc and JSON. | +| `linux-gcc-neutral` | Ubuntu 24.04 x64, GCC 14 Release | Headless Engine and JSON, hwloc omitted. | +| `linux-clang-debug` | Ubuntu 24.04 x64, Clang 18 Debug | Headless Engine, hwloc/JSON, ASan/UBSan. | +| `windows-msvc-hwloc` | Windows 2025 x64, MSVC RelWithDebInfo | Headless Engine and hwloc/JSON with vcpkg native libraries. | +| `linux-modules` | Ubuntu 24.04 x64, GCC 14 Release | Native GLFW/input, OpenGL, TGUI, RmlUi, miniaudio, hwloc/JSON and SDK. | +| `windows-modules` | Windows 2025 x64, MSVC RelWithDebInfo | The same selected modules/helpers and SDK, with bundled native dependencies. | +| `linux-modules-shared` | Ubuntu 24.04 x64, GCC 14 Release | The modules/SDK composition with shared Cheryl libraries and owned dependencies. | +| `windows-modules-shared` | Windows 2025 x64, MSVC RelWithDebInfo | The shared modules/SDK composition, including DLL runtime discovery. | + +[run.py](../../.github/ci/run.py) configures explicit owners/options/build targets, +runs each focused owner prefix once and executes independent consumers. Modules +jobs compile the demo for composition coverage but never launch or package it. +Linux jobs provide TrueType/CFF allocation fixtures. Missing fixtures and +platform-specific skips are reported as unaccepted coverage. Native OpenGL, native +hwloc binding acceptance, actual audio devices, desktop/controller QA and the +Debug terminal remain separate from this focused CI selection. + +Modules jobs package and verify an extracted SDK on every validation run. +[verify_sdk.py](../../.github/ci/verify_sdk.py) checks the checksum and absence of +producer paths, then builds independent consumers/header probes using only installed +Cheryl libraries. Test fixture sources/fonts come from the checkout; Cheryl and +module implementation sources are not rebuilt. Use a fresh consumer build/extraction +directory for each verification. See +[repeating SDK checks](architecture-validation.md#installed-sdk-consumers). + +Successful main-push modules jobs retain SDK candidates for three days. After all +headless and modules jobs succeed, the collection job verifies checksums and uploads +`cheryl-development-sdks-` for 90 days. Test/configuration reports are retained +for 14 days. These are workflow artifacts; the workflow creates no GitHub release, +uploads no release assets and publishes no demo package. Release publication is a +separate future decision. + +The first main-push artifact collection remains +[TR20](../testing-requests.md#tr20-automated-main-push-sdk-collection); collection is +intentionally skipped on PR, `develop` and manual runs. + +### Actions cache retention + +Compiler and dependency caches speed up subsequent CI runs. They are separate +from the SDK and diagnostic artifacts above, and from each other: + +| Cache | Key family | Contents and ownership | +| --- | --- | --- | +| Compiler results | `ccache-v2` | Main, develop and PR layers, described below. | +| X: build dependencies | `build-deps-v2` | Separate main/develop submodule Git data and Windows vcpkg package archives/downloads. PRs read their base branch's X. | +| Y: Actions environment | `actions-env-cache-v2` | Shared tools, Linux package downloads, Python wheels/downloads and prepared documentation tools, published by main. | + +Each compiler matrix configuration has separate ccache layers: + +| Build | Readable layers | Writable layer saved after building | +| --- | --- | --- | +| `main` | Main baseline A | A | +| `develop` | A + develop additions B | B | +| PR targeting `develop` | A + B + PR additions C | C | +| PR targeting `main` | A + C | C | + +[restore-compiler-cache](../../.github/actions/restore-compiler-cache/action.yml) +restores the available layers into separate directories. +[compiler_cache.py](../../.github/ci/compiler_cache.py) configures ccache to check +the current layer first, followed by its parents, and write compilation results +only into the current layer. Parent hits stay in the parent layers. Statistics +live outside the archived data. Before saving, the helper removes compiled +results already available under the same cache key in a parent. It preserves +distinct lookup metadata, then trims the writable layer to 1 GiB. +An unchanged layer is not uploaded again. A layer that becomes empty replaces its +previous snapshot with a small empty-layer marker, allowing cleanup to remove +the older duplicated data. + +This hierarchy requires `main` to be GitHub's default branch. A PR can read its +own cache, its base branch's cache and the default branch's cache; a PR targeting +`main` cannot read `develop`'s cache. Manual feature-branch builds use their own +writable layer and the main baseline. These are +[GitHub cache scope restrictions](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache). +Missing parents cause normal cache misses until their builds populate them. +The `ccache-v2` keys distinguish this file-backend layout from the previous +ordinary local-cache archives; the first build of each configuration seeds the +new format. Compiler layers and Windows binary dependency snapshots use new +run-specific keys because saved GitHub caches are immutable. + +Cache X's source key hashes `.gitmodules` and the recorded submodule commits. +Updating a submodule selects a new source snapshot automatically; an older X can +seed Git objects before checkout fetches the requested commits. The binary +component follows the vcpkg manifest and runner image, with vcpkg validating each +package's binary compatibility when using a fallback snapshot. Vendored libraries +remain ordinary CMake targets and reuse their compiler results through ccache. +Only main/develop push and manual runs publish X; PRs fetch or build missing +dependencies locally. + +To update Y, change package/tool selections in +[environment.json](../../.github/ci/environment.json), generator packages in +[requirements-modules.txt](../../.github/ci/requirements-modules.txt), or the +documentation tool manifests. Bump `cache_version` from `2` to `3` when revising +the Actions environment or forcing a refresh; this changes the family to +`actions-env-cache-v3`. Environment inputs are also hashed so package changes +cannot silently reuse a mismatched key. Y has separate immutable components for +each OS/architecture, runner image and environment composition. Resolved Linux +package versions, Python ABI and tool manifests further identify their payloads; +submodule commits and run IDs do not affect Y. + +Every branch restores Y, while only trusted main push/manual runs publish it. +Develop and PR changes to the environment install normally on a cache miss; +main publishes the updated Y after those changes reach it. Package installers +still run on each fresh runner; cached downloads avoid fetching the same packages +again. Documentation tooling is prepared and cached before document generation, +so a later documentation failure does not discard the prepared environment. + +[cache-maintenance.yml](../../.github/workflows/cache-maintenance.yml) runs after +the build or documentation workflow finishes and daily at 04:23 UTC. It removes +caches belonging to closed pull requests, keeps the newest mutable compiler and +dependency snapshot per branch, configuration and cache version, and trims older caches +toward an 8 GiB repository budget. Active workflow refs and caches created or +accessed within the last 15 minutes are protected. After removing redundant +snapshots, budget eviction removes ordinary old caches first, then the latest +open-PR compiler/dependency snapshots, then develop snapshots, and finally main +snapshots. Known default-branch tooling caches have the same priority as develop +layers. Each tier uses oldest access first. The newest X source snapshot per +branch and runner receives that branch's priority; older submodule pins remain +ordinary eviction candidates. Old-format compiler snapshots lose their branch +priority once the same branch and configuration has a new-format replacement. +These priorities preserve useful branch caches where space allows; the budget +can still require evicting a current layer. The budget is a target: concurrent +uploads and protected caches can temporarily keep usage above it. +Cleanup deletes cached build inputs, without deleting SDK artifacts or changing +build results; an evicted cache is regenerated when next needed. + +To inspect or apply cleanup, open **Actions → Maintain Actions caches → Run +workflow** after this workflow has reached the repository's default branch. +Leave **dry_run** enabled to preview cache IDs, keys, refs, sizes and deletion +reasons. Optionally enter a closed PR number in **closed_pr** to restrict the +operation to that PR's caches. Review the run's report, then dispatch again with +**dry_run** disabled to apply the current plan. Automatic runs apply cleanup; +they execute the default branch's script without loading PR code, artifacts or +cached executables. + +GitHub also [expires unused caches and evicts old entries at its storage +limit](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#usage-limits-and-eviction-policy). +The maintenance policy removes redundant snapshots before that limit causes +frequent replacement of useful caches. + +### Hosted Windows coverage limits + +Both Windows profiles leave these focused cases unaccepted: + +| Cases | Missing prerequisite and next action | +| --- | --- | +| `font_bake.real_nested_allocation_scope`, `font_bake.real_cff_alloc_failure` | The workflow supplies the bundled TrueType face but no CFF OpenType fixture on Windows. Supply `CHERYL_STB_ALLOCATION_CFF` with a suitable face, retain the TrueType fixture for nested scopes, and rerun these cases on hosted Windows. | +| `logging.config_reopen` | The test needs a temporary directory on the checkout's filesystem root so it can use a relative log path. Configure the hosted Windows temporary directory on that drive, then rerun the case. | + +The [font fixture requirements](architecture-validation.md#native-and-fixture-checks) +define suitable faces. A green job does not accept these skipped cases; successful +local MSVC/Wine checks do not replace the missing hosted execution. These limits +are separate from SDK collection and desktop/GPU/device acceptance. diff --git a/docs/development/consuming-engine.md b/docs/development/consuming-engine.md index bc17a1b1..03ed5d4b 100644 --- a/docs/development/consuming-engine.md +++ b/docs/development/consuming-engine.md @@ -41,6 +41,9 @@ Replace `path/to/cheryl-engine` with the checkout path relative to your applicat `CMakeLists.txt`, or an absolute path. CMake builds the selected libraries and propagates their public include paths and dependency settings. Reload the CMake project in the IDE after changing selections so completion uses those settings. +Install shared development packages on Linux or prepare the Windows dependencies +with the toolchain/helper arguments in +[project setup](../getting-started/project-setup.md#build-your-game-with-a-source-checkout). Keep `CHERYL_BUILD_SDK=OFF` for source application builds; aggregate headers and IDE completion do not require SDK export or installation. Enable it only when also producing a separate SDK. The [SDK build](building.md#development-sdk-builds) @@ -53,32 +56,22 @@ targets below when you want to choose support and diagnostic behavior individual ### Windows source-build runtime -Shared libraries must be available when the game starts. With -`add_subdirectory(... cheryl)` and a single-configuration Ninja build in -`build/release`, Cheryl writes its DLLs to `build/release/cheryl`; the game's -executable can be in the parent directory. Multi-configuration generators add a -configuration subdirectory, such as `cheryl/Release`. +Shared libraries must be available when the game starts, including with static +Cheryl. Source composition stages selected package DLLs into component and +executable output directories. It also stages each executable's CMake-known +runtime DLLs, so a game in the parent build directory can find Cheryl and its +owned toolkit libraries from a nested `add_subdirectory(... cheryl)` build. +This staging runs independently of SDK installation. -After creating and linking `my_game`, copy CMake-known runtime DLLs beside it: +The staging helper follows imported DLL dependencies, including optional native +package dependencies, and reports missing or conflicting runtime files. OS and +MSVC runtime DLLs remain platform prerequisites. -```cmake -get_target_property(cheryl_engine_type Cheryl::Engine TYPE) -if(WIN32 AND cheryl_engine_type STREQUAL "SHARED_LIBRARY") - add_custom_command(TARGET my_game POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy -t "$" - "$" - COMMAND_EXPAND_LISTS - VERBATIM) -endif() -``` - -This stages Cheryl and dependency DLLs whose runtime locations are known to CMake. -Native FreeType, HarfBuzz or ICU packages can still require their SDK's DLL -directories on the process `PATH`: CMake's -[`TARGET_RUNTIME_DLLS`](https://cmake.org/cmake/help/v3.28/manual/cmake-generator-expressions.7.html#genex:TARGET_RUNTIME_DLLS) -cannot list DLLs for imported targets without runtime-location metadata. -Alternatively, configure the IDE/launcher `PATH` with both Cheryl's configuration -output directory and the native SDK's DLL directories instead of copying. +Keep the staged DLLs beside the executable when launching from an IDE or copying +the build to another directory. The host needs the matching MSVC runtime and +Windows 10 version 1903 or later; ICU comes from the operating system. CMake +targets for supplied shared libraries must identify their runtime DLL and import +library so staging can locate the runtime artifacts. ### Granular composition @@ -89,6 +82,9 @@ This example selects neutral Engine alone: set(CHERYL_BUILD_TESTS OFF) set(CHERYL_BUILD_DEMO OFF) set(CHERYL_BUILD_NATIVE_GLFW OFF) +set(CHERYL_BUILD_STARTUP OFF) +set(CHERYL_STARTUP_OPENGL OFF) +set(CHERYL_BUILD_WORKERS OFF) set(CHERYL_BUILD_DEBUG_TERMINAL_LINUX OFF) set(CHERYL_BUILD_OPENGL OFF) set(CHERYL_BUILD_UI_TGUI OFF) @@ -106,8 +102,10 @@ 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. +For command-line startup, select `CHERYL_BUILD_STARTUP` and link `Cheryl::Startup`; +`CHERYL_STARTUP_OPENGL` chooses its private configured native graphics integration. +The compatibility `Cheryl::OpenGL::Startup` target also supplies the native/graphics +public usage requirements. Consumers obtain C++23, includes, logging policy and SDK requirements through targets rather than compiling Engine sources or repeating dependency lists. The [module guide](modules.md) describes ownership, standalone composition and tests. @@ -139,12 +137,17 @@ For a Linux Release SDK, configure the application's directory as follows: ) ``` -The SDK contains compiled Engine, Startup and selected module/topology libraries, -their dependency libraries or headers, the Engine crash bootstrap, public headers, +The SDK contains compiled Engine, Startup and selected module libraries, +their required public dependency headers, the Engine crash bootstrap, 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. +Embedded implementations such as miniaudio, Gainput, GLAD and spdlog do not +become application header or runtime-target requirements. GLFW remains a shared +runtime used through Native GLFW. Public header/template +frontends such as CTTI, GLM and CLI11, plus native toolkit authoring headers, +retain their usage requirements. Static SDKs also package the private archive +closure required by final executable linkage. Its `Cheryl::Cheryl` target selects the components distributed in that SDK; `` exposes their public interfaces. Installed headers share the `include/cheryl/` namespace, including the module and support umbrellas described @@ -178,40 +181,42 @@ mixing are outside this SDK contract. | 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; shared Cheryl DLLs also live in the SDK's `bin/`. Both DLL directories must be on the application's 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. -For an HID-enabled SDK with an owned hidapi dependency, the selected backend, -headers and licenses are packaged. Linux consumers also need pkg-config and the -backend's libudev/libusb development libraries, plus Iconv when selected; the -package config discovers only the dependencies that backend requires. Packaging +| Linux | Install the matching shared FreeType, HarfBuzz and ICU runtimes, plus hwloc/HIDAPI when selected and a compatible graphics/display stack. Owned GLFW/UI toolkit shared libraries ship with the SDK. Static Cheryl consumers additionally need the corresponding development packages and packaged embedded archives for final linking. | +| Windows | Deploy the SDK's selected Cheryl, toolkit and external native DLLs, and install the matching MSVC runtime. Native dependencies keep shared linkage with static Cheryl too. Windows 10 version 1903+ supplies ICU; no separate ICU DLL is distributed. | + +Shared package configuration does not rediscover private FreeType, HarfBuzz, ICU +or hwloc development packages, but their matching runtime ABI must be available. +Static Linux packages discover those development packages for final linking and +check the producer's ICU major version. Static Windows packages resolve native +import libraries supplied with the SDK. Embedded archives and explicit system +dependencies complete static Cheryl's link closure. Native HID system/backend +requirements remain selected-platform requirements. Packaging does not resolve the [Linux controller limitations](../../projects/modules/platform/native-glfw/README.md#controller-backend-limits). -The package exposes `Cheryl_RUNTIME_DIRECTORIES`: shared SDKs use `lib/` on Linux -and `bin/` on Windows; Windows bundles also include `dependencies/bin/`. +The package exposes `Cheryl_RUNTIME_DIRECTORIES` for its packaged shared libraries: +`lib/` on Linux and `bin/` on Windows. This can be nonempty for static Cheryl SDKs. Use those paths for application runtime setup or deploy the required libraries beside your executable. Linux shared libraries find their packaged neighbors -through an origin-relative runtime path. Windows config also prepends the bundled -dependency prefix for CMake discovery; it does not deploy DLLs automatically. +through an origin-relative runtime path. SDK import configuration does not copy +DLLs beside the application's executable 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](building.md#github-ci-and-sdk-artifacts) -retains hosted skipped-case limits and the pending main-push artifact collection. +Use installed-target consumers and first-include probes to check each selected +SDK composition, including Startup and native toolkit authoring. The +[CI guide](ci.md) retains hosted coverage limits. 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`](../../projects/engine/support/startup/include/cheryl/core/engine/startup.h) +[`CE::Application::Startup`](../../projects/modules/runtime/startup/include/cheryl/application/startup.h) 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](../../projects/modules/graphics/opengl/README.md#command-line-startup). -The support targets expose CLI11 in their public headers; linking Engine or OpenGL +before invoking a selected backend factory. Link `Cheryl::Startup` and include +`` for the standard +`CE::Application::make_startup(description, ApplicationOptions)` path. Startup +privately owns its configured GLFW/OpenGL integration. `Cheryl::OpenGL::Startup` +remains a compatibility interface without another runtime binary. +Startup exposes CLI11 in its public option frontend; linking Engine or OpenGL alone retains their direct context/runtime construction APIs without that public dependency. @@ -230,8 +235,10 @@ 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: +`CE::Application::ApplicationOptions` supplies initial window/presentation, +`execution` and `runtime` defaults for the generic factory. The lower-level +`Startup(StartupBackend, ..., StartupOptions)` constructor remains available for +custom composition. Common Engine options override their runtime defaults: | Argument | Effect | | --- | --- | @@ -258,10 +265,13 @@ 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](../../projects/apps/demo/src/demo-options.cpp) +return an owned context; a null result fails startup. Standard selection uses +`ApplicationOptions::backend` (`Default` or `GLFWOpenGL`) and the configured +OpenGL integration accepts `--backend default|opengl`. A Startup binary built +with that integration requires the module binaries even if a run never creates +a window; omit them by building Startup without that integration. Runtime-loaded +discovery/hot unloading is outside this contract. +[The demo option helper](../../projects/apps/demo/src/demo-options.cpp) shows application-specific registration without duplicating the engine parser. ## Game hooks and ownership @@ -292,28 +302,35 @@ when Escape is pressed. It submits no draws yet: ```cpp #include -#include +#include class Game final : public CE::Game::AbstractGame { CE::Runtime::Context& engine_; static constexpr CE::Input::Actions::Id quit_{1}; + CE::Input::Actions::BindingId quit_binding_ = 0; public: explicit Game(CE::Runtime::Context& engine) : engine_(engine) {} void init() override { auto& input = engine_.input(); - static_cast(input.bindings().bind_button({input.keyboard_id(), gainput::KeyEscape}, quit_)); + quit_binding_ = input.bindings().bind_button( + {input.keyboard_id(), CE::Input::KeyboardKey::Escape}, 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 {} + void deinit() override { + if (quit_binding_ != 0) { + static_cast(engine_.input().bindings().unbind(quit_binding_)); + quit_binding_ = 0; + } + } }; int main(int argc, char** argv) { - auto startup = CE::Application::GLFWOpenGL::make_startup("My game", {.title = "My game"}); + auto startup = CE::Application::make_startup("My game", {.title = "My game"}); auto result = startup.initialize(argc, argv); if (!result.should_start()) return result.exit_code; @@ -325,7 +342,7 @@ int main(int argc, char** argv) { ``` Link this example to `Cheryl::Cheryl` with OpenGL selected, or to -`Cheryl::OpenGL::Startup` for granular composition. Pass `--concurrent` to select the +`Cheryl::Startup` with OpenGL integration for granular composition. 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. @@ -360,7 +377,7 @@ SDK installation collects the roots into one physical `include/cheryl/` tree. | `` | Engine plus the modules and support selected by linked targets. | | `` | All neutral Engine public headers. | | `` | Modules selected by linked targets. | -| `` | Support libraries selected by linked targets. | +| `` | Compatibility support umbrellas selected by linked targets. | | `` and other `cheryl/modules/` umbrellas | One module's public headers. | | `cheryl/support/` umbrellas | One support library's public headers. | @@ -371,23 +388,25 @@ or link a module: CMake target selection supplies its dependencies and the feature definitions used by the group umbrellas. Link `Cheryl::Cheryl` for all configured components, or link granular public targets and use the same umbrellas for that narrower selection. Additional includes such -as `` remain appropriate when directly using dependency APIs. +as native TGUI/RmlUi headers remain available for their deliberately public +authoring APIs; private GLFW/Gainput/GLAD/spdlog headers are not consumer requirements. | 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. | +| CTTI, GLM | Explicit Engine public header/template requirements; legacy `glm.hpp` spelling remains supported. | +| spdlog and Backward | Engine private logging/diagnostic implementation; callback/severity types belong to Cheryl and native sink access is private. | | 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. | +| FreeType, HarfBuzz, ICU | Engine private shared font/layout implementation and link requirements; Windows uses OS ICU. 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. | +| GLFW | Shared runtime used by Native GLFW; OpenGL calls exported native operations. | +| Gainput/HID | Gainput is embedded in Native GLFW; HIDAPI is shared when selected. Public input types, binding mappings and device snapshots belong to Cheryl. | | 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. | +| GLAD and OpenGL::GL | GLAD is private compiled OpenGL implementation; the graphics driver/system runtime remains explicit. | | 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. | +| hwloc 2.5+ (2.x) | Workers module private topology implementation; its JSON feature remains independently selectable. | +| TGUI / RmlUi | Shared toolkit libraries expose native authoring APIs. Their Cheryl adapters supply Engine font/image services through private callbacks, preserving one Engine implementation even with static Cheryl. Matching toolkit headers/ABI settings are public requirements. | The Engine umbrellas and granular contracts are neutral. Concrete window/input headers belong to Native GLFW; `backends/opengl` headers belong to OpenGL. Existing @@ -448,8 +467,9 @@ The Engine-only consumer entry point is: ) ``` -Standalone consumer entry points require -`CHERYL_REPOSITORY_ROOT=/path/to/cheryl-engine` for shared helpers and bootstrapping. +Standalone consumer entry points locate their enclosing checkout automatically; +`CHERYL_REPOSITORY_ROOT=/path/to/cheryl-engine` explicitly selects another checkout +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. @@ -481,7 +501,7 @@ An existing CMake/CLion profile can retain failed C++ compiler ABI discovery. Ch 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 +GCC can then fail to resolve `math.h` or `stdlib.h` through `#include_next` in C++ standard headers. Refresh compiler discovery and regenerate the build files while preserving the diff --git a/docs/development/contributing.md b/docs/development/contributing.md index 9b9dd924..188c656d 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -1,6 +1,6 @@ # Contributing -Start with the [architecture overview](../runtime/runtime-architecture.md) and the +Start with the [architecture overview](../architecture/assembling-engine.md) and the guide for the subsystem you are changing. Choose the owning library before adding code: neutral contracts belong in Engine, runtime integration SDKs in their modules, optional Engine helpers in support targets, and game mechanics in the diff --git a/docs/development/documentation.md b/docs/development/documentation.md index 81301cc7..91599d15 100644 --- a/docs/development/documentation.md +++ b/docs/development/documentation.md @@ -1,4 +1,4 @@ -# Generating and publishing documentation +# Generating documentation The manual and C++ reference share one generation command. Doxygen extracts the selected public headers to XML; Breathe and Exhale turn that XML into Sphinx pages, @@ -113,6 +113,10 @@ completed stages; fixing the cause and rerunning still performs all checks. Cached rendering does not mean the output passed validation. Tool-installation caches are separate from generated-site caches and survive clean generation. +Use `--jobs 4` to let Sphinx read and write documents with up to four workers. +The default is one; the count must be a positive integer. This selects rendering +parallelism without changing the generated content or validation requirements. + The log explains why XML or Sphinx output needs regeneration and reports the time spent extracting, rendering and checking. `Generating fresh Sphinx output` means the saved environment is being replaced; Sphinx then reports every guide @@ -266,136 +270,8 @@ changing one direct version and accepting whatever an installer resolves: when changing Doxygen, Breathe or Exhale. A new requirements hash selects a new cache identity; old caches do not need to be overwritten. -## Publishing contract - -The publishing configuration is -[.readthedocs.yaml](../../.readthedocs.yaml), -[documentation.yml](../../.github/workflows/documentation.yml) and -[sync-docs-stable.py](../../.github/scripts/sync-docs-stable.py). Hosted publication -still requires the account and repository setup below. Both `develop` and `main` -must contain the engine headers and documentation implementation before their -respective sites can build. - -GitHub Actions generates and checks HTML for pull requests, pushes to `develop` -and `main`, `v*` tags, and manual runs. It retains checked HTML and build diagnostics -as downloadable artifacts for 30 days. Read the Docs independently clones the -selected Git ref, runs the same pinned generator, and hosts the resulting HTML. -Both publication paths request full generation with `--clean`. -Actions artifacts are not the website's source and do not need an upload token for -Read the Docs. Engine compilation belongs to the existing code CI; the documentation -job does not compile examples. - -Direct pushes to other branches do not schedule these validation jobs. Pull -requests targeting `develop` or `main` still run them. Keep workflow definitions -valid on every pushed branch: GitHub creates a failed run for each new commit -containing an invalid workflow, even on a branch excluded by its filters. Changing -branch filters or job conditions does not suppress those configuration errors; -correct the invalid YAML or expression. See -[GitHub's workflow-log guidance](https://docs.github.com/en/actions/how-tos/monitor-workflows/use-workflow-run-logs). - -The Read the Docs configuration uses Ubuntu 24.04 and Python 3.13, with one custom -`build.jobs.build.html` command: - -```yaml -version: 2 -build: - os: ubuntu-24.04 - tools: - python: "3.13" - jobs: - build: - html: - - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean --output "$READTHEDOCS_OUTPUT" -``` +## Publish the generated site -Keep `sphinx`, `mkdocs` and a separate Python installation step out of this custom -configuration: bootstrap owns setup and generation. Read the Docs publishes the -`html/` directory beneath `$READTHEDOCS_OUTPUT`. Its -[custom build documentation](https://docs.readthedocs.com/platform/stable/build-customization.html) -and [configuration reference](https://docs.readthedocs.com/platform/stable/config-file/v2.html#build-jobs) -describe this contract. PDF and other builders are outside the current configuration. - -### Version policy - -| Website version | Git source | Policy | -| --- | --- | --- | -| `latest` | `develop` | Current development documentation. | -| `stable` | Explicit `stable` branch | Fast-forward mirror of a checked commit on `main`. | -| Release version, such as `vX.Y.Z` | Matching release tag | Historical documentation, activated individually or by an agreed tag rule. | - -Set the Read the Docs default branch to `develop`. An explicit Git branch named -`stable` supplies the stable version instead of Read the Docs automatically choosing -the greatest stable semantic version tag. Do not create a conflicting `stable` tag. -Activate desired historical tags and leave them visible for the version selector; -tags predating the documentation tooling cannot generate a site without a separate -backport policy. See [Read the Docs versions](https://docs.readthedocs.com/platform/stable/versions.html). - -The stable sync job is disabled unless the repository variable -`CHERYL_DOCS_PUBLISH_STABLE` equals the literal string `true`. Enable it only after -approval, the documentation merge into `main`, and account setup. The job must run -only for canonical-repository pushes to `main`, after documentation checks succeed; -it must never check out pull-request code with write credentials. Serialize stable -updates and compare the checked revision with the live `main` ref before updating. -Create `stable` if absent, otherwise fast-forward it without force. Reject divergence -and respect repository rules; do not delete, reset or force-update the branch to -bypass a rejection. GitHub's [ref API](https://docs.github.com/en/rest/git/refs#update-a-reference) -supports the required non-forced update. - -Read the Docs builds `latest` directly from `develop`; that build is independent of -the Actions result. The stable mirror advances only after the documentation job -succeeds. Branch protection and release policy determine which engine checks must -pass before a change reaches `main`. - -### First account and site setup - -1. Merge the documentation configuration into `develop`. Create an account at - [Read the Docs Community](https://app.readthedocs.org/), install its GitHub App - with access to `cppcooper/cheryl-engine`, and import that repository. Importing - requires repository administration access. The App subscribes to the required - events, so no additional manual webhook is needed. If the project was imported - manually, connect the repository in project settings. See the - [Git integration instructions](https://docs.readthedocs.com/platform/stable/reference/git-integration.html#adding-a-project-from-a-repository). -2. Record the actual project slug and public URL returned by Read the Docs; do not - assume the slug from the repository name. Set its default branch to `develop`, - activate `latest`, and check that version using the publication checks below. -3. Merge the approved release into `main`. Enable `CHERYL_DOCS_PUBLISH_STABLE=true` - only when stable publication is approved. Enabling the variable does not - trigger a workflow, and manual dispatch runs only the documentation checks. - The next qualifying `main` push creates or advances the `stable` branch; let - Read the Docs sync versions, then activate `stable`. Trigger a build to refresh - versions if it has not appeared yet. -4. After a successful stable build and revision check, choose `stable` as the - project's default website version. Activate existing release tags manually; - a future-tag activation rule does not replace that initial selection. - -### Publication verification and recovery - -Every successful HTML generation writes `html/revision.json`. Its -`source_revision` is the full checked Git commit, `source_ref` identifies the build -ref, and `dirty` records whether the source checkout differs from that commit. -The marker also records selected tool versions and lock/manifest hashes, without -machine-specific executable or cache paths. Source links must use the same full -commit; an overridden revision must match Git `HEAD`. - -For the first site and each publication investigation, fetch `revision.json` from -the actual version URL, for example `/revision.json`. Compare -`source_revision` exactly with the expected commit and the Read the Docs build's -reported commit; a successful build status or plausible page content is insufficient. -For `stable`, also compare the Git `stable` and intended checked `main` revisions. -Publication requires a clean source marker, successful internal links and fragments, -and Git source links resolving to objects at that commit. - -First-site acceptance also includes opening the manual and API entry points, -following an inherited base link and a declaring-header link, checking navigation -and search, and switching between `latest` and `stable` once both are active. -Check historical-version selection when a release tag containing the documentation -tooling is available; do not count that coverage before such a tag exists. Confirm -the GitHub App triggers the expected branch rebuilds. Local HTML checks do not -establish that the hosted version selector, search or webhook delivery works. - -To pause stable updates, set `CHERYL_DOCS_PUBLISH_STABLE` to `false`. Correct a bad -publication with a new reviewed commit on `main`, including a revert when restoring -earlier content, then let the normal fast-forward path rebuild `stable`. Do not move -the stable branch backward. If a ref diverges or repository rules reject an update, -inspect the refs and resolve the cause before retrying; do not force the mirror. -After recovery, repeat the exact revision and page checks against the public URL. +The [publishing guide](publishing-documentation.md) owns account setup, version +policy, stable promotion and hosted recovery. Local generation and link checks +prepare the artifact; they do not publish a website. diff --git a/docs/development/logging-acceptance.md b/docs/development/logging-acceptance.md index 21fbe106..a6b94f79 100644 --- a/docs/development/logging-acceptance.md +++ b/docs/development/logging-acceptance.md @@ -53,7 +53,7 @@ compile stripping. Record these distinctions instead of claiming zero skips. Independent processes exercise standard/non-standard sink log/flush failures, failed rotation leaving a closed file, saturated recursive native submission, -Block and DiscardNew loss behavior, discarded flush requests, retained clones, +Block and DiscardNew loss behavior, discarded flush requests, retained backend ownership, static singleton/pool teardown, and fatal final owner destruction from a callback. Named-routing acceptance separately checks the five engine files, formatted/stream/ direct named writes, category filtering and closed/reopened behavior after the diff --git a/docs/development/modules.md b/docs/development/modules.md index 196d5c31..48ad2d09 100644 --- a/docs/development/modules.md +++ b/docs/development/modules.md @@ -14,22 +14,25 @@ The optional Linux Debug terminal owns process output/viewer behavior outside th runtime adapter graph. Its Engine support attachment accepts a module callback; an absent implementation leaves application startup unchanged. -Command-line bootstrap uses the separate `Cheryl::Startup` and -`Cheryl::OpenGL::Startup` support targets. They add CLI11 and backend argument -definitions to applications that select them; Engine and OpenGL do not depend -back on those targets. See [startup composition](consuming-engine.md#command-line-startup). - -Optional Engine helpers can use `projects/engine/support/` without becoming runtime -platform modules. Preserve the same dependency direction: selected support targets -consume Engine; Engine does not acquire their SDKs or initialization as a reverse -dependency. `Cheryl::WorkerTopology::Hwloc` and `Cheryl::WorkerTopology::Json` -use that boundary for independently selected discovery/binding and configuration -helpers. [Provider composition](../runtime/worker-execution.md#topology-providers-and-capacity) -is application-owned; neutral workers and scheduling remain in the one Engine library. +The public [Startup module](../modules/startup.md), `Cheryl::Startup`, supplies +standard application bootstrap and privately integrates configured backends. +`Cheryl::OpenGL::Startup` is a compatibility interface over Startup/OpenGL rather +than another runtime library. CLI11 remains the explicit public option frontend; +Engine and OpenGL do not depend back on Startup. + +The optional [Workers module](../modules/workers.md), `Cheryl::Workers`, groups +configuration loading and topology integration with independent JSON/hwloc +features. Core pool execution/scheduling remains in Engine. Its capability-specific +`Cheryl::WorkerTopology::Hwloc` and `Cheryl::WorkerTopology::Json` compatibility +names refer to the same selected implementation. Private support belongs inside +its owning binary; configurable build policy alone is not a public module. ```mermaid flowchart LR Application --> Engine + Application --> Startup + Startup --> Engine + Startup --> OpenGL Application --> NativeGLFW Application --> OpenGL OpenGL --> NativeGLFW @@ -41,6 +44,30 @@ flowchart LR Audio --> Engine ``` +## Adapter contracts + +Use [Interfaces and Adapters](../architecture/interfaces-adapters.md) to find the +required runtime interfaces, optional capabilities, existing implementations and +replacement dependencies. [Assembling the Engine](../architecture/assembling-engine.md) +explains how factories connect them. This guide covers creating and composing the +module that owns an implementation. + +### SDL3 scaffold + +`CHERYL_BUILD_NATIVE_SDL3=ON` adds the source-only +`Cheryl::NativeSDL3::Scaffold` interface target from +[`platform/native-sdl3`](../../projects/modules/platform/native-sdl3). +The option defaults to `OFF`. Its +[`cheryl/backends/native-sdl3.h`](../../projects/modules/platform/native-sdl3/include/cheryl/backends/native-sdl3.h) +includes the existing Engine contracts and forward-declares +`CE::Platform::SDL3::Window`, `DisplaySystem` and `InputSystem`. + +The scaffold has no runtime implementations, SDL3 dependency discovery or startup +factory. It is not registered as an SDK component or a selectable application +backend. Native GLFW remains the implemented platform integration. SDL3 runtime +implementation, OpenGL/startup selection and GLFW deprecation policy remain +future work. + ## Module boundary criteria A separate module is justified when it gives an application a concrete choice or @@ -72,10 +99,10 @@ because they have few callers. ## Select an assembly -The root defaults to Native GLFW, OpenGL, TGUI, RmlUi and miniaudio; the demo is +The root defaults to Startup, Workers, Native GLFW, OpenGL, TGUI, RmlUi and miniaudio; the demo is opt-in. UI and audio selection is independent of platform/graphics selection. OpenGL requires Native GLFW; the demo also requires native input. For Engine alone, -disable every module and both worker-support options as shown in the +disable every module, including Startup/Workers, as shown in the [engine-only configuration](building.md#engine-only-configuration). On Linux GNU/Clang this includes `CHERYL_BUILD_DEBUG_TERMINAL_LINUX=OFF`. The root automatically links the selected terminal interface to its demo/shared @@ -91,7 +118,7 @@ target_link_libraries(game PRIVATE Cheryl::Engine Cheryl::NativeGLFW Cheryl::Ope ``` Alternatively, link `Cheryl::Cheryl` to select the complete configured assembly, -including its available support libraries: +including its selected public capabilities: ```cmake target_link_libraries(game PRIVATE Cheryl::Cheryl) @@ -120,15 +147,23 @@ granular owner headers, such as `cheryl/backends/native-glfw.h`. a supplied GLFW target keeps its host settings. The [build guide](building.md) owns their configuration and dependency details. +Bundled compiled dependencies have one Cheryl binary owner: Engine owns logging, +font/text and image implementations; Native GLFW owns GLFW/Gainput/HID; OpenGL +owns GLAD; each UI module owns its toolkit implementation and exports its native +authoring API. Startup owns selected integration glue and Workers owns optional +topology/configuration adaptation. Header/template usage such as GLM, CTTI and +CLI11 remains explicitly public. See [dependency contracts](consuming-engine.md#dependencies-and-headers). + Selection preserves owner lifetimes: input detaches while its window is live, resources retire before context destruction, and runtime shutdown settles workers and recycles frames before game/adapter cleanup. ## Standalone modules -Standalone entry points require an absolute `CHERYL_REPOSITORY_ROOT` for shared -helpers and Engine bootstrapping. Each owner reuses a supplied `Cheryl::Engine` -or adds Engine from that root. OpenGL also reuses `Cheryl::NativeGLFW` or requires +Standalone entry points locate the enclosing checkout's shared helpers and Engine +automatically. Set an absolute `CHERYL_REPOSITORY_ROOT` when selecting another +checkout. Each owner reuses a supplied `Cheryl::Engine` or adds Engine from that +root. OpenGL also reuses `Cheryl::NativeGLFW` or requires `CHERYL_NATIVE_GLFW_SOURCE`. Bootstrapping suppresses other owners, demo and Engine tests in a local variable scope, without rewriting a host's cache. diff --git a/docs/development/publishing-documentation.md b/docs/development/publishing-documentation.md new file mode 100644 index 00000000..8729a374 --- /dev/null +++ b/docs/development/publishing-documentation.md @@ -0,0 +1,140 @@ +# Publishing documentation + +Publish the generated manual and API as a versioned Read the Docs site. Prepare +the artifact with [documentation generation](documentation.md), then use the +account, version and verification procedures below. Hosted setup requires the +repository/account permissions stated in the setup procedure. + +## Publishing contract + +The publishing configuration is +[.readthedocs.yaml](../../.readthedocs.yaml), +[documentation.yml](../../.github/workflows/documentation.yml) and +[sync-docs-stable.py](../../.github/scripts/sync-docs-stable.py). Hosted publication +still requires the account and repository setup below. Both `develop` and `main` +must contain the engine headers and documentation implementation before their +respective sites can build. + +GitHub Actions generates and checks HTML for pull requests, pushes to `develop` +and `main`, `v*` tags, and manual runs. It retains checked HTML and build diagnostics +as downloadable artifacts for 30 days. Read the Docs independently clones the +selected Git ref, runs the same pinned generator, and hosts the resulting HTML. +Both publication paths request full generation with `--clean`. +Actions artifacts are not the website's source and do not need an upload token for +Read the Docs. Engine compilation belongs to the existing code CI; the documentation +job does not compile examples. + +Direct pushes to other branches do not schedule these validation jobs. Pull +requests targeting `develop` or `main` still run them. Keep workflow definitions +valid on every pushed branch: GitHub creates a failed run for each new commit +containing an invalid workflow, even on a branch excluded by its filters. Changing +branch filters or job conditions does not suppress those configuration errors; +correct the invalid YAML or expression. See +[GitHub's workflow-log guidance](https://docs.github.com/en/actions/how-tos/monitor-workflows/use-workflow-run-logs). + +The Read the Docs configuration uses Ubuntu 24.04 and Python 3.13, with one custom +`build.jobs.build.html` command: + +```yaml +version: 2 +build: + os: ubuntu-24.04 + tools: + python: "3.13" + jobs: + build: + html: + - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean --output "$READTHEDOCS_OUTPUT" +``` + +Keep `sphinx`, `mkdocs` and a separate Python installation step out of this custom +configuration: bootstrap owns setup and generation. Read the Docs publishes the +`html/` directory beneath `$READTHEDOCS_OUTPUT`. Its +[custom build documentation](https://docs.readthedocs.com/platform/stable/build-customization.html) +and [configuration reference](https://docs.readthedocs.com/platform/stable/config-file/v2.html#build-jobs) +describe this contract. PDF and other builders are outside the current configuration. + +### Version policy + +| Website version | Git source | Policy | +| --- | --- | --- | +| `latest` | `develop` | Current development documentation. | +| `stable` | Explicit `stable` branch | Fast-forward mirror of a checked commit on `main`. | +| Release version, such as `vX.Y.Z` | Matching release tag | Historical documentation, activated individually or by an agreed tag rule. | + +Set the Read the Docs default branch to `develop`. An explicit Git branch named +`stable` supplies the stable version instead of Read the Docs automatically choosing +the greatest stable semantic version tag. Do not create a conflicting `stable` tag. +Activate desired historical tags and leave them visible for the version selector; +tags predating the documentation tooling cannot generate a site without a separate +backport policy. See [Read the Docs versions](https://docs.readthedocs.com/platform/stable/versions.html). + +The stable sync job is disabled unless the repository variable +`CHERYL_DOCS_PUBLISH_STABLE` equals the literal string `true`. Enable it only after +approval, the documentation merge into `main`, and account setup. The job must run +only for canonical-repository pushes to `main`, after documentation checks succeed; +it must never check out pull-request code with write credentials. Serialize stable +updates and compare the checked revision with the live `main` ref before updating. +Create `stable` if absent, otherwise fast-forward it without force. Reject divergence +and respect repository rules; do not delete, reset or force-update the branch to +bypass a rejection. GitHub's [ref API](https://docs.github.com/en/rest/git/refs#update-a-reference) +supports the required non-forced update. + +Read the Docs builds `latest` directly from `develop`; that build is independent of +the Actions result. The stable mirror advances only after the documentation job +succeeds. Branch protection and release policy determine which engine checks must +pass before a change reaches `main`. + +### First account and site setup + +1. Merge the documentation configuration into `develop`. Create an account at + [Read the Docs Community](https://app.readthedocs.org/), install its GitHub App + with access to `cppcooper/cheryl-engine`, and import that repository. Importing + requires repository administration access. The App subscribes to the required + events, so no additional manual webhook is needed. If the project was imported + manually, connect the repository in project settings. See the + [Git integration instructions](https://docs.readthedocs.com/platform/stable/reference/git-integration.html#adding-a-project-from-a-repository). +2. Record the actual project slug and public URL returned by Read the Docs; do not + assume the slug from the repository name. Set its default branch to `develop`, + activate `latest`, and check that version using the publication checks below. +3. Merge the approved release into `main`. Enable `CHERYL_DOCS_PUBLISH_STABLE=true` + only when stable publication is approved. Enabling the variable does not + trigger a workflow, and manual dispatch runs only the documentation checks. + The next qualifying `main` push creates or advances the `stable` branch; let + Read the Docs sync versions, then activate `stable`. Trigger a build to refresh + versions if it has not appeared yet. +4. After a successful stable build and revision check, choose `stable` as the + project's default website version. Activate existing release tags manually; + a future-tag activation rule does not replace that initial selection. + +### Publication verification and recovery + +Every successful HTML generation writes `html/revision.json`. Its +`source_revision` is the full checked Git commit, `source_ref` identifies the build +ref, and `dirty` records whether the source checkout differs from that commit. +The marker also records selected tool versions and lock/manifest hashes, without +machine-specific executable or cache paths. Source links must use the same full +commit; an overridden revision must match Git `HEAD`. + +For the first site and each publication investigation, fetch `revision.json` from +the actual version URL, for example `/revision.json`. Compare +`source_revision` exactly with the expected commit and the Read the Docs build's +reported commit; a successful build status or plausible page content is insufficient. +For `stable`, also compare the Git `stable` and intended checked `main` revisions. +Publication requires a clean source marker, successful internal links and fragments, +and Git source links resolving to objects at that commit. + +First-site acceptance also includes opening the manual and API entry points, +following an inherited base link and a declaring-header link, checking navigation +and search, and switching between `latest` and `stable` once both are active. +Check historical-version selection when a release tag containing the documentation +tooling is available; do not count that coverage before such a tag exists. Confirm +the GitHub App triggers the expected branch rebuilds. Local HTML checks do not +establish that the hosted version selector, search or webhook delivery works. + +To pause stable updates, set `CHERYL_DOCS_PUBLISH_STABLE` to `false`. Correct a bad +publication with a new reviewed commit on `main`, including a revert when restoring +earlier content, then let the normal fast-forward path rebuild `stable`. Do not move +the stable branch backward. If a ref diverges or repository rules reject an update, +inspect the refs and resolve the cause before retrying; do not force the mirror. +After recovery, repeat the exact revision and page checks against the public URL. diff --git a/docs/development/testing.md b/docs/development/testing.md index 329fbd81..a0cd244c 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -81,7 +81,7 @@ separates enabled/disabled dependency selections and controlled/native coverage. ## CI and installed SDK checks -[GitHub CI](building.md#github-ci-and-sdk-artifacts) selects owner prefixes rather +[GitHub CI](ci.md) selects owner prefixes rather than aggregate duplicates. Headless jobs select Engine, logging, Engine acceptance, JSON/composition and the worker consumer. Modules jobs additionally select Native GLFW, OpenGL, both UI owners, miniaudio and coexistence, and run their independent diff --git a/docs/development/ui-adapters.md b/docs/development/ui-adapters.md index 7fa8b49b..f7ae14c5 100644 --- a/docs/development/ui-adapters.md +++ b/docs/development/ui-adapters.md @@ -15,7 +15,12 @@ candidate; it does not define player-facing widget architecture. ## Module and application ownership Give the adapter one library target, `include/`, `src/` and owner-local `tests/`. -Link its public target to `Cheryl::Engine` and the selected toolkit. Follow the +Link its public target to `Cheryl::Engine` and select toolkit linkage according to +that library's convention. The current TGUI and RmlUi integrations use shared +toolkit libraries and expose their native authoring headers/API through the +adapter target. Keep Engine-dependent service implementations in the adapter; +private versioned callbacks let the toolkit call those services without linking +another Engine copy when Cheryl itself is static. Follow the [module guide](modules.md) for supplied targets, standalone composition and dependency selection. Configure only an explicitly selected dependency; ordinary CMake configuration does not download it or change a host's toolkit settings. @@ -65,7 +70,7 @@ The independent implementations differ where their toolkits differ: | --- | --- | --- | | Authoring | Native GUI/widgets; optional typed numeric placement and resizing. | Native context/documents; RML and RCSS layout, including percentage bounds and flex. | | Alpha | Straight vertex/image RGBA and `StraightAlpha` materials. | Premultiplied output and `PremultipliedAlpha` materials; file images convert once. | -| Fonts | Guarded FreeType backend and embedded default font. | Stock Core FreeType engine and explicitly registered owned font files/bytes. | +| Fonts | Engine rasterization through the custom TGUI font backend; embedded default font. | Module-owned interface retaining reviewed provider/atlas/effects/fallback over Engine rasterization; explicitly registered owned font bytes. | | Sampling | Each draw retains nearest/linear smoothing independently of pixels; UI samplers clamp, with no mipmaps or anisotropy. | Existing image upload defaults; repeating UV/effect extensions reject. | | Toolkit lifetime | One guarded process-global backend. | One guarded Core initialization with contexts and interfaces. | @@ -161,7 +166,7 @@ supplied package needs its own applicable proof. Reuse `build/testing-native-ui` when its source, toolchain and configuration match. This Linux Release selection compiles both adapters, their consumers/probes, the -coexistence suite, OpenGL startup support and the UI-enabled demo with Cheryl-owned +coexistence suite, Startup with OpenGL integration and the UI-enabled demo with Cheryl-owned TGUI/RmlUi dependencies. It uses GLFW/X11/OpenGL with HID disabled. Initialize the pinned dependencies from the [build guide](building.md); RmlUi cases also require the SDK sample font or an explicit `CHERYL_RMLUI_TEST_FONT`. The executed consumers diff --git a/docs/diagnostics/index.md b/docs/diagnostics/index.md new file mode 100644 index 00000000..45e0f338 --- /dev/null +++ b/docs/diagnostics/index.md @@ -0,0 +1,17 @@ +# Diagnostics + +Configure useful output, inspect subsystem observations, and preserve failure +context through cleanup. The [Diagnostics system](../engine-systems/diagnostics.md) +introduces the available snapshots and their observation boundaries. + +```{toctree} +:maxdepth: 1 + +Configure logging and investigate missing records <../runtime/logging> +Investigate runtime and resource symptoms <../runtime/subsystem-diagnostics> +Handle native and cleanup failures <../runtime/failure-reporting> +``` + +A log record or counter is evidence about an operation, not a completion barrier. +Use the owning operation's future or lifecycle when another step depends on its +completion. diff --git a/docs/engine-systems/assets.md b/docs/engine-systems/assets.md new file mode 100644 index 00000000..1a87cbab --- /dev/null +++ b/docs/engine-systems/assets.md @@ -0,0 +1,107 @@ +# Assets + +## Purpose + +Assets discovers source files and manifests, prepares owned CPU data and +publishes reusable images, sprites, tilesets, shaders and materials. Application +classes keep their own position, behavior and animation state while sharing +immutable assets. + +## Interface + +Types below are in `CE::Assets` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:struct}`Loading::Loader `, {cpp:struct}`Loading::Prepared `
Prepare owned CPU input, upload on the provider owner, or synchronously `load_assets()`. [Loading entry point](../assets/asset-loading.md#runtime-entry-point); [publication and retry](../assets/asset-loading.md#publication-and-retry). | +| {cpp:class}`Loading::FileRegistry `, {cpp:struct}`Manifests::Graphics `
Discover indexed files and retain parsed manifest metadata. [Index selection](../assets/asset-manifests.md#index-selection-and-paths); [typed consumers](../assets/asset-loading.md#typed-manifest-data-and-consumers). | +| {cpp:struct}`Sprites::Manager `, {cpp:struct}`Tilesets::Manager `
Retrieve shared sprite/tile assets; validate handles before use. [Resources and caches](../assets/asset-values-and-playback.md#resources-and-caches); [sprite task](../getting-started/how-to/draw-sprite.md). | +| {cpp:struct}`Sprites::Definition `, {cpp:struct}`Tilesets::Definition `, {cpp:struct}`Grids::Definition `
Describe immutable atlas grids, cells, clips and tile rules. [Grid coordinates](../assets/asset-values-and-playback.md#coordinates-and-grid-geometry); [manifest schema](../assets/asset-manifests.md). | +| {cpp:class}`Sprites::Animation `, {cpp:struct}`Tilesets::Animation `
Keep independent playback cursors over shared clip metadata. [Sprite playback](../assets/asset-values-and-playback.md#sprite-playback); [tile playback](../assets/asset-values-and-playback.md#tile-playback-and-rules). | + +### Selection values and enums + +| Type | Choices and details | +| --- | --- | +| {cpp:type}`Grids::CellIndex `, {cpp:struct}`Grids::View ` | Address cells and resolve authored grid geometry/UVs. [Grid and cell addressing](../assets/asset-manifests.md#grid-and-cell-addressing); [coordinates](../assets/asset-values-and-playback.md#coordinates-and-grid-geometry). | +| {cpp:enum}`Tilesets::Direction ` | Eight compass neighbors used by tile selection. [Tile selection](../assets/asset-values-and-playback.md#tile-selection). | +| {cpp:enum}`Tilesets::BitmaskType `, {cpp:enum}`Tilesets::WangType ` | Four/eight-neighbor masks or corner/edge Wang rules. [Tile rules](../assets/asset-values-and-playback.md#tile-playback-and-rules); [autotile schema](../assets/asset-manifests.md#autotiles). | +| {cpp:struct}`Sprites::Clip `, {cpp:struct}`Sprites::Profile ` | Select named clip timing and application-selected animation profiles. [Animations](../assets/asset-manifests.md#animations); [animation task](../getting-started/how-to/animate-sprite.md). | + +## Usage + +### Uses + +File discovery and manifests supply CPU inputs; Resources uploads and publishes +them. Applications can place CPU preparation on Workers and transfer owned +results through Dispatch. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Files::FileIndex`
{cpp:struct}`CE::Rendering::Resources::Provider`
{cpp:struct}`CE::Rendering::Resources::Image`
{cpp:struct}`CE::Rendering::Geometry::Buffer2D`
{cpp:class}`CE::Rendering::Resources::Material`
{cpp:struct}`CE::Memory::Objects::Pool` | [Resources](resources.md)
[Memory](memory.md)
[Utilities](utilities.md)
[State](state.md)
[Diagnostics](diagnostics.md)
[Workers](workers.md)
[Dispatch](dispatch.md) | + +### Used by + +Application-owned objects retain visible assets; submission resolves them into +draw packets. Tools can inspect prepared manifests without graphics. Text keeps +its own font-selection/layout path. + +| Types | Systems | +| --- | --- | +| [`Tutorial::Player`](../getting-started/create-a/player.md)
[`Tutorial::Enemy`](../getting-started/create-a/enemy.md)
[`Tutorial::Hud`](../getting-started/create-a/hud.md)
{cpp:struct}`CE::Rendering::DrawPacket2D` | [Rendering](rendering.md)
[Text](text.md) | + +## Contracts + +Preparation changes no global caches or native state. Upload is serialized on +the active provider/loading owner. Successful entries can become visible before +the whole upload finishes; failure does not imply rollback. Retain a prepared +copy before consuming it when retry needs that exact input. Existing keys and +explicit shader replacement have different update rules. + +An owned loader binds one immutable root, but another loader does not create a +second resource domain. The shared caches currently support one active +provider/loading-owner domain. Retained definitions and handles remain distinct +from a live backend's ability to use them. + +## Lifecycle + +Create an owned loader for an asset root, prepare CPU input and upload after +renderer initialization. Validate retrieved handles before constructing game +objects. Release the game's handles during cleanup; published frames may retain +their own copies until recycled. Native resource retirement follows the backend +domain, not loader destruction or the last manager lookup. + +## Execution + +CPU preparation may run on workers. Cache publication and resource upload obey +provider thread/current-context requirements. Advance animation and select +visible state on simulation; frame preparation copies selected cells and styles +without changing essential game state. + +## Integration + +On the platform owner during `init()`: + +```cpp +CE::Assets::Loading::Loader loader(asset_root_); +loader.load_assets(engine_.resources()); +auto sprite = CE::Assets::Sprites::Manager::get().get_asset("first-game", "actor"); +``` + +Check `sprite` and required materials before constructing your player. Keep its +position/animation cursor in the player, not in shared asset metadata. See +[loading and drawing a sprite](../getting-started/how-to/draw-sprite.md). + +## Limits and further reading + +There is no engine-owned map/world or built-in Player/Enemy class. Tile selection +operates on application-owned map data. Read [loading](../assets/asset-loading.md), +[manifests](../assets/asset-manifests.md), +[asset values and playback](../assets/asset-values-and-playback.md), +[file/font discovery](../assets/file-and-font-discovery.md), +[asset catalog](../assets/catalog.md) and +[residency](../resources/resource-residency.md). The +[Loader header](../../projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h) +defines preparation/publication entry points. diff --git a/docs/engine-systems/audio.md b/docs/engine-systems/audio.md new file mode 100644 index 00000000..5a057e91 --- /dev/null +++ b/docs/engine-systems/audio.md @@ -0,0 +1,100 @@ +# Audio + +## Purpose + +Audio plays independent voices from immutable clips or streamed files. Playback +has its own output clock and application-owned lifetime rather than depending +on render-frame publication or a runtime context adapter. + +## Interface + +Types below are in `CE::Audio` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`Clip `, {cpp:struct}`Format `
Own immutable interleaved float PCM and source rate/channel metadata through `from_samples()`. [Clips and voices](../runtime/audio.md#clips-and-voices). | +| {cpp:class}`System `
Play clips/streams, set master volume, maintain retired voices and close output. [Lifetime and scheduling](../runtime/audio.md#lifetime-and-scheduling); [adapter catalogue](../architecture/interfaces-adapters.md#integrations-alongside-the-runtime). | +| {cpp:class}`Voice `, {cpp:struct}`VoiceSnapshot `
Control independent playback and obtain a copied state/volume/loop/duration observation. [Playback controls](../runtime/audio.md#clips-and-voices). | +| {cpp:class}`Miniaudio::System `
Select device or offline output; use module decoding for file clips. [miniaudio setup](../modules/miniaudio.md#preparation-and-output). | + +### Playback values and choices + +| Type or choice | Purpose and details | +| --- | --- | +| {cpp:struct}`PlaybackOptions ` | Initial normalized volume, looping and paused state per voice. [Clip/stream submission](../runtime/audio.md#clips-and-voices). | +| {cpp:enum}`PlaybackState ` | `Playing`, `Paused`, `Finished`, `Stopped`, `Closed`; stopped/finished voices require explicit restart. [State transitions and closed handles](../runtime/audio.md#clips-and-voices). | +| Device / offline output | Device output advances independently; offline output advances only through explicit `render()` calls. [Backend selection](../runtime/audio.md#backends-and-limits); [miniaudio preparation](../modules/miniaudio.md#preparation-and-output). | +| Clip / streamed file | Retained decoded samples or file-backed playback whose source must remain available. [Sources and limits](../runtime/audio.md#clips-and-voices). | + +## Usage + +### Uses + +The selected module owns mixing, decoding and output. Workers are an optional +place for synchronous decode/file preparation; playback advances on the audio +output clock. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Audio::Clip`
{cpp:struct}`CE::Audio::Format`
{cpp:struct}`CE::Audio::PlaybackOptions`
[`std::filesystem::path`](../runtime/audio.md#clips-and-voices) | [Workers](workers.md) | + +### Used by + +Applications start sound effects and manage music voices. Voices can share a +clip with independent cursors. Runtime hooks provide useful lifecycle +boundaries, but the application closes audio explicitly; audio-only programs +need no display or graphics. + +| Types | Systems | +| --- | --- | +| [Application-owned sound/music controllers](../runtime/audio.md#clips-and-voices) | — | + +## Contracts + +Clips require complete nonempty finite mono/stereo PCM at supported source +rates. Volume is finite normalized linear gain in `[0, 1]`; mixing loud sources +can still exceed nominal amplitude. A stream file must remain available through +playback. Device opening, decode and initial stream opening can block. + +Dropping a voice handle does not stop active playback. Stop and natural finish +require explicit restart; toggling looping alone does not restart them. +Snapshots observe playback rather than freezing its native cursor. + +## Lifecycle + +Create output before submitting voices. Retain handles for voices needing later +control and call `maintain()` regularly to reclaim eligible unreferenced voices. +Maintenance does not advance audio time. Settle producers before system +destruction; idempotent `close()` stops output and retires voices before native +state. Surviving handles report `Closed` and reject controls. Construct a new +system for another session. + +## Execution + +Controls, submissions, snapshots and maintenance synchronize application +producers. Playback advances independently on the backend clock, without calling +game callbacks from mixing. Runtime concurrency does not change the audio +ownership contract. + +## Integration + +With an application-owned `audio` system and previously prepared `clip`: + +```cpp +auto voice = audio.play(clip, {.volume = 0.5f}); +// Keep voice if the game needs to pause, stop or change this playback. +audio.maintain(); +``` + +Create the system and decode clips at loading boundaries. Trigger prepared clips +from game state, then close output only after all producers stop. + +## Limits and further reading + +Offline processing is explicit; an unavailable device must not silently succeed +inaudibly. Spatial/world/collision behavior belongs to game code or another +selected capability. Read [audio ownership](../runtime/audio.md), +[miniaudio setup and coverage](../modules/miniaudio.md) and +[the System/Voice interface](../../projects/engine/include/cheryl/audio/system.h). diff --git a/docs/engine-systems/diagnostics.md b/docs/engine-systems/diagnostics.md new file mode 100644 index 00000000..f259bf1a --- /dev/null +++ b/docs/engine-systems/diagnostics.md @@ -0,0 +1,102 @@ +# Diagnostics + +## Purpose + +Diagnostics exposes operational observations: session counts, queue activity, +resource retirement, worker policy, asset publication and memory bookkeeping. +It helps correlate a failure without changing the owning operation's semantics. + +## Interface + +Types below are in `CE::Diagnostics` unless another namespace is shown. + +### Types and observations + +| API and purpose | +| --- | +| {cpp:type}`DomainId `
Correlate one process-local owner through `diagnostic_id()` or snapshot domain fields. [Domain identities](../runtime/subsystem-diagnostics.md). | +| {cpp:struct}`CE::Runtime::RuntimeStats`
Inspect completed-session update, poll, frame and pressure counts. [Runtime statistics](../runtime/subsystem-diagnostics.md#runtime-statistics). | +| {cpp:struct}`CE::Workers::GroupStatus`, {cpp:struct}`DispatchStats `
Observe CPU work, queue activity and reported policy behavior. [Worker statistics](../runtime/subsystem-diagnostics.md#worker-statistics); [dispatcher statistics](../runtime/subsystem-diagnostics.md#dispatcher-statistics). | +| {cpp:struct}`CE::Events::EventStats`
Observe registration, dispatch and delivery activity. [Event statistics](../runtime/subsystem-diagnostics.md#event-statistics). | +| {cpp:struct}`CE::Assets::Loading::UploadStats`, {cpp:struct}`CE::Memory::Stats`
Inspect asset publication and synchronized CPU allocation bookkeeping. [Asset diagnostics](../runtime/subsystem-diagnostics.md#asset-diagnostics); [memory statistics](../runtime/subsystem-diagnostics.md#memory-statistics). | + +### Observation boundaries + +| Observation | Purpose and details | +| --- | --- | +| `GameRuntime::diagnostics()` | Owned completed-session counts after synchronization with run completion; live sampling rejects. [Runtime statistics](../runtime/subsystem-diagnostics.md#runtime-statistics). | +| Queue/group status | Activity at the snapshot boundary; an empty queue does not prove detached work finished. [Worker](../runtime/subsystem-diagnostics.md#worker-statistics) and [dispatcher](../runtime/subsystem-diagnostics.md#dispatcher-statistics) limits. | +| Resource retirement statistics | Tracked native resources and retirement/collection activity in one backend domain. [Native resource statistics](../runtime/subsystem-diagnostics.md#native-resource-statistics). | +| Explicit diagnostic report | A bounded operational summary outside sensitive locks/native callbacks; required emergency failure output has a separate path. [Failure reporting](../runtime/failure-reporting.md#emergency-reporting); [validation limits](../runtime/subsystem-diagnostics.md#validation-and-limits). | + +## Usage + +### Uses + +Observers read snapshots under each owner's synchronization and observation +limits. Ordinary summaries use Logging; bounded emergency output preserves +required failure context independently. + +| Types | Systems | +| --- | --- | +| {cpp:struct}`CE::Runtime::RuntimeStats`
{cpp:struct}`CE::Workers::GroupStatus`
{cpp:struct}`CE::Diagnostics::DispatchStats`
{cpp:struct}`CE::Events::EventStats`
{cpp:struct}`CE::Assets::Loading::UploadStats`
{cpp:struct}`CE::Memory::Stats`
{cpp:class}`CE::Logging::Logger` | [Runtime](runtime.md)
[Workers](workers.md)
[Dispatch](dispatch.md)
[Events](events.md)
[Assets](assets.md)
[Memory](memory.md)
[Resources](resources.md)
[Logging](logging.md) | + +### Used by + +Subsystem owners use domain identities and expose counters for applications, +hosts and developers. Acceptance needs observations of the selected behavior; +quiet, skipped or unselected paths do not establish it. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Runtime::GameRuntime`
{cpp:class}`CE::Workers::Pool`
{cpp:class}`CE::Events::EventBus`
{cpp:struct}`CE::Rendering::Resources::Provider`
{cpp:struct}`CE::Assets::Cache::Store`
{cpp:struct}`CE::Memory::Manager` | [Runtime](runtime.md)
[Workers](workers.md)
[Dispatch](dispatch.md)
[Events](events.md)
[Assets](assets.md)
[Memory](memory.md)
[Resources](resources.md)
[Display](display.md)
[Rendering](rendering.md)
[Logging](logging.md) | + +## Contracts + +Counters are observations, not completion barriers, admission limits or resource +budgets. Domain IDs are monotonic process-local identities, not addresses/native +handles or durable keys; zero indicates exhaustion. Unavailable observations +must remain explicit. + +`GameRuntime::diagnostics()` requires caller synchronization with run start and +return; live sampling rejects. An empty dispatcher queue does not establish that +detached work completed. Asset completion counts do not establish batch rollback +or distinguish every reused key from a newly published entry without their +documented counters. + +## Lifecycle + +An owner acquires its identity with construction and publishes observations at +documented boundaries. Take a completed runtime snapshot after `run()` returns +or throws, retaining the runtime through that query. Explicit reports must run +outside containing locks/native callbacks according to the subsystem contract. +Retained snapshots own their values without retaining the observed service. + +## Execution + +Each snapshot follows the observed owner's access rules. Reporting can allocate, +format or log, so it is not suitable for arbitrary destructor/native callback +contexts. Emergency reporting has a deliberately separate bounded contract and +must not mask the original exception. + +## Integration + +After synchronizing with runtime completion: + +```cpp +const auto stats = runtime.diagnostics(); +const auto updates = stats.updates; +const auto rendered = stats.rendered; +``` + +Use these values in an application report with the run outcome. Different +update/render counts are expected with independent scheduling and skipped or +superseded visual snapshots. + +## Limits and further reading + +Counters do not measure cache speedups, native thread residency or unselected +hardware behavior. Read [subsystem observation limits](../runtime/subsystem-diagnostics.md), +[failure reporting](../runtime/failure-reporting.md), +[architecture acceptance limits](../development/architecture-validation.md#evidence-limits) +and [diagnostic declarations](../../projects/engine/include/cheryl/core/diagnostics.h). diff --git a/docs/engine-systems/dispatch.md b/docs/engine-systems/dispatch.md new file mode 100644 index 00000000..b4bacdee --- /dev/null +++ b/docs/engine-systems/dispatch.md @@ -0,0 +1,100 @@ +# Dispatch + +## Purpose + +Dispatch transfers owned work to the platform/graphics or simulation owner. +Use it when an operation requires that owner, rather than running arbitrary CPU +work through an owner queue or calling a thread-affine adapter directly. + +## Interface + +Types below are in `CE::Runtime` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`PlatformDispatcher `
Submit work taking `Context&` to platform/graphics through the context. [Platform dispatch](../runtime/thread-dispatch.md). | +| {cpp:class}`SimulationDispatcher `
Submit work without a context argument to simulation through the runtime. [Simulation boundary](../runtime/thread-dispatch.md#simulation-boundary). | +| {cpp:class}`PlatformDispatcher::Submission `, {cpp:class}`SimulationDispatcher::Submission `
Save copyable submission endpoints that reject safely after closure/destruction. [Saved endpoints and cancellation](../runtime/thread-dispatch.md). | +| {cpp:struct}`CE::Diagnostics::DispatchStats`
Inspect accepted/completed/pending observations without treating queue emptiness as completion. [Dispatcher statistics](../runtime/subsystem-diagnostics.md#dispatcher-statistics). | + +### Submission and completion + +| Operation | Purpose and details | +| --- | --- | +| `submit()` | Decay-own work/captures and retain its future; borrowed pointees still need a lifetime. [Ownership and future results](../runtime/thread-dispatch.md). | +| `submission()` | Retain queue state independently of the borrowed service facade. [Endpoint lifetime](../runtime/thread-dispatch.md). | +| Future readiness / result | Consume accepted results/errors without waiting on the owner needed to produce them. [Dependency progress](../runtime/runtime-frame-boundary.md#queued-preparation-and-stop-requests). | +| Runtime closure | Settle required accepted dependencies, then cancel unexecuted work before game cleanup. [Shutdown order](../runtime/runtime-frame-boundary.md#shutdown). | + +## Usage + +### Uses + +Runtime opens, drains and closes queues on their selected owners. Submitted +platform work receives its live Context. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Runtime::Context`
{cpp:struct}`CE::Diagnostics::DispatchStats`
{cpp:type}`CE::Diagnostics::DomainId` | [Runtime](runtime.md)
[Diagnostics](diagnostics.md) | + +### Used by + +Consumers send owned uploads or simulation changes to the required owner. +Platform callbacks may operate Resources or Display through Context. Event +delivery remains composed outside the bus; general parallel CPU work belongs to +Workers. + +| Types | Systems | +| --- | --- | +| {cpp:type}`CE::Events::EventBus::Delivery`
{cpp:class}`CE::UI::TGUI::SceneUploader`
{cpp:class}`CE::UI::RmlUi::SceneUploader`
[`Tutorial::Hud`](../getting-started/create-a/hud.md) | [Events](events.md)
[Assets](assets.md)
[Text](text.md)
[GUI](gui.md)
[Resources](resources.md) | + +## Contracts + +Each drain detaches one FIFO batch. New/nested posts wait for another drain; +submission never executes inline. Captures are decay-owned, but references and +reference-valued results still borrow targets. Accepted exceptions reach the +future; unexecuted cancellation breaks its promise. Never wait on a future from +the owner needed to complete it. + +Endpoints retain queue state rather than borrowed service pointers. They reject +before opening, after closing and after destruction. `has_pending()` only sees +queued work; an empty queue does not establish completion of a detached batch. + +## Lifecycle + +Runtime opens queues during initialization and binds simulation to its selected +owner. Save endpoints for longer-lived adapters without extending backend +lifetime. During shutdown, runtime services accepted dependencies while joining +simulation and settling tracked CPU work, then cancels remaining requests before +game cleanup. Borrowed captures must survive either execution or cancellation. + +## Execution + +Platform requests run with the runtime's selected graphics context on platform. +Simulation batches drain before scheduled updates without consuming input merely +because mailbox work ran. Cross-domain ordering follows explicit dependencies; +there is no total order between independent platform, simulation and worker work. + +## Integration + +From another producer, submit a window observation to its owner: + +```cpp +auto pending = engine_.platform_dispatcher().submit([](CE::Runtime::Context& owner) { + return owner.window().framebuffer_size(); +}); +``` + +Inspect future readiness from later updates; do not block simulation while it +depends on platform progress. Ordinary game layout should use tick dimensions +directly; the example shows where an explicitly requested live observation runs. + +## Limits and further reading + +Dispatch is not a general executor, a completion barrier based on queue emptiness +or automatic lifetime extension for captured raw pointers. Read +[thread dispatch](../runtime/thread-dispatch.md), +[runtime shutdown](../runtime/runtime-frame-boundary.md#shutdown) and +[the platform interface](../../projects/engine/include/cheryl/core/engine/platform-dispatcher.h). diff --git a/docs/engine-systems/display.md b/docs/engine-systems/display.md new file mode 100644 index 00000000..5404bca1 --- /dev/null +++ b/docs/engine-systems/display.md @@ -0,0 +1,100 @@ +# Display + +## Purpose + +Display manages native windows, monitor inventory, modes and dimensions. It +supplies the window used by input and presentation; rendering is a separate +responsibility. + +## Interface + +Types below are in `CE::Platform` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`DisplaySystem `
Enumerate monitors, create/select display-owned windows and query monitor scale. [Inventory and selection](../runtime/display-and-window-contract.md#native-glfw-inventory-and-selection); [adapter catalogue](../architecture/interfaces-adapters.md#runtime-adapters). | +| {cpp:class}`Window `
Read size/mode/close state, request resize/mode/cursor changes and check deferred failures. [Sizes, modes and observation](../runtime/display-and-window-contract.md#sizes-modes-and-observation). | +| {cpp:struct}`Monitor `
Inspect a borrowed monitor inventory entry; copy values before deferred use. [Monitor identity and freshness](../runtime/display-and-window-contract.md#native-glfw-inventory-and-selection). | +| {cpp:struct}`WindowResized `, `CE::Platform::window_resized_event`
Receive copied drawable pixels and a display-borrowed window pointer. [Resize observations](../runtime/display-and-window-contract.md#sizes-modes-and-observation); [typed channels](../runtime/event-delivery.md#typed-channels). | + +[Native GLFW](../modules/native-glfw.md) implements these desktop contracts. + +### Dimensions and modes + +| Type or choice | Purpose and details | +| --- | --- | +| {cpp:struct}`Dimensions ` | Logical window coordinates; creation/resize requests need positive dimensions. [Size units](../runtime/display-and-window-contract.md#sizes-modes-and-observation). | +| {cpp:struct}`FramebufferSize ` | Drawable pixels, which may be zero; runtime copies them into ticks. [Framebuffer observations](../runtime/display-and-window-contract.md#sizes-modes-and-observation). | +| {cpp:enum}`WindowMode ` | `NORMAL`, `BORDERLESS`, `FULLSCREEN`; backend policy determines supported transitions. [Window modes](../runtime/display-and-window-contract.md#sizes-modes-and-observation). | +| Monitor content scale | X/Y scale for a recognized monitor snapshot; separate from per-window notification and explicit UI font scale. [Scale and UI capabilities](../runtime/display-and-window-contract.md#scale-and-ui-capabilities). | + +## Usage + +### Uses + +The selected platform implementation owns native library/window state and +publishes resize observations through Events. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Events::EventChannel`
{cpp:struct}`CE::Platform::Monitor`
{cpp:struct}`CE::Platform::Dimensions`
{cpp:struct}`CE::Platform::FramebufferSize` | [Events](events.md) | + +### Used by + +Runtime composes display with input and presentation. Cameras and UI consume +copied dimensions; live window operations remain on the platform owner. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Runtime::Context`
{cpp:class}`CE::Input::System`
{cpp:class}`CE::Rendering::OpenGL::GLFWContext`
{cpp:class}`CE::Rendering::Camera2D`
{cpp:class}`CE::UI::TGUI::Session`
{cpp:class}`CE::UI::RmlUi::Session` | [Runtime](runtime.md)
[Input](input.md)
[Rendering](rendering.md)
[GUI](gui.md) | + +## Contracts + +Window dimensions use logical window coordinates; framebuffer dimensions use +drawable pixels and may be zero. Creation/resize requests require positive +logical dimensions. Monitor inventory references are borrowed and expire on +backend refresh or destruction. Copy values before deferred use. + +Monitor content scale is not a per-window scale notification contract. Window +activation alone does not switch a renderer's graphics-resource domain. A +resize payload's window pointer neither extends its lifetime nor grants access +from another thread. + +## Lifecycle + +The display owns created windows; pointers returned to callers are borrowed. +Construct compatible input/presentation dependents while the display is live. +Detach callbacks and close graphics dependents before destroying their window. +Runtime coordinates the normal session cleanup; application-supplied services +must preserve the same order during partial initialization and failure. + +## Execution + +Native display/window operations run on the platform owner. Queries do not +synchronize access from simulation. Resize callbacks remain on platform; pass +their copied observations through runtime input/frame handoff rather than +mutating simulation state directly. + +## Integration + +Update a simulation-owned camera from the runtime's copied observation: + +```cpp +void update(const CE::Game::TickContext& tick) override { + camera_.set_framebuffer_size(tick.framebuffer_size); + // Update the game without querying engine_.window() here. +} +``` + +Game initialization can read the live window on platform. Later updates use the +tick in either runtime mode, so the same game remains safe when concurrent. + +## Limits and further reading + +Read [display/window contracts](../runtime/display-and-window-contract.md) for +monitor freshness, mode transitions, scale and native failure limits. The +[Window](../../projects/engine/include/cheryl/core/display/window-interface.h) +and [DisplaySystem](../../projects/engine/include/cheryl/core/display/display-system-interface.h) +headers define the portable interface; module details stay in the owner guide. diff --git a/docs/engine-systems/events.md b/docs/engine-systems/events.md new file mode 100644 index 00000000..48e5c64e --- /dev/null +++ b/docs/engine-systems/events.md @@ -0,0 +1,108 @@ +# Events + +## Purpose + +Events delivers notifications through persistent registrations. A bus owns +callbacks and channel identity; it does not create a thread, choose simulation +timing or automatically own the objects borrowed by those callbacks. + +## Interface + +Types below are in `CE::Events` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`EventBus `
Own listener registrations, dispatch, remove/wait and close acceptance. [Registrations](../runtime/event-delivery.md#registrations); [removal and shutdown](../runtime/event-delivery.md#removal-and-shutdown). | +| {cpp:class}`EventChannel `
Give a copy-constructible payload exact name/type identity. [Typed channels](../runtime/event-delivery.md#typed-channels). | +| {cpp:class}`EventBus::Registration `
Retain an explicit removal identity; dropping it does not unregister. [Registration lifetime](../runtime/event-delivery.md#registrations). | +| {cpp:type}`EventBus::Delivery `, {cpp:type}`EventBus::ErrorHandler `
Select queued execution and an independent failure sink. [Queued delivery](../runtime/event-delivery.md#optional-queued-delivery); [adapter catalogue](../architecture/interfaces-adapters.md#integrations-alongside-the-runtime). | +| {cpp:struct}`EventSystem `
Access the default bus when an isolated application-owned bus is unnecessary. [Bus ownership](../runtime/event-delivery.md). | + +### Delivery and cleanup choices + +| Choice | Purpose and details | +| --- | --- | +| Immediate delivery | Invoke on each dispatch caller, in registration order; concurrent callers may overlap. [Immediate delivery](../runtime/event-delivery.md#immediate-delivery). | +| Platform / simulation delivery | Queue owned payloads through a saved owner endpoint; never execute inline. [Queued adapters](../runtime/event-delivery.md#optional-queued-delivery); [dispatch](../runtime/thread-dispatch.md). | +| Worker delivery | Give one listener stream serial FIFO execution over shared group capacity. [Worker streams](../runtime/event-delivery.md#worker-stream-acceptance-and-cancellation). | +| Unregister / unregister-and-wait / close | Invalidate new invocation, optionally settle entered callbacks, or close all acceptance. [Removal and shutdown](../runtime/event-delivery.md#removal-and-shutdown). | + +## Usage + +### Uses + +Payload types define owned and borrowed values. Dispatch/Workers dependencies +belong to optional delivery adapters; the bus defaults to immediate delivery. +Diagnostics observes activity without owning queued failure handling. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Events::EventChannel`
{cpp:type}`CE::Diagnostics::DomainId`
{cpp:class}`CE::Runtime::PlatformDispatcher::Submission`
{cpp:class}`CE::Runtime::SimulationDispatcher::Submission`
{cpp:class}`CE::Workers::Group` | [Diagnostics](diagnostics.md)
[Dispatch](dispatch.md)
[Workers](workers.md) | + +### Used by + +Display publishes the typed resize channel. Games and services publish domain +events; queued subscriptions can deliver them to the selected owner without +native callbacks mutating game state directly. + +| Types | Systems | +| --- | --- | +| {cpp:struct}`CE::Events::EventSystem`
{cpp:class}`CE::Platform::GLFW::Window`
[Application-owned listeners](../runtime/event-delivery.md#typed-channels) | [Display](display.md) | + +## Contracts + +Registration persists until explicit removal or bus destruction; dropping its +identifier does not unregister. Immediate callbacks run in registration order +on each dispatch caller, and overlapping dispatches can overlap listeners. +Typed channels require the exact copy-constructible payload type. + +Unregister prevents fresh invocation but a callback already entered may finish. +Use `unregister_and_wait()` before destroying a borrowed target, without holding +a lock that callback needs. Waiting from that same listener rejects. Queued +payloads own their copied values; pointees remain governed by the payload type. + +## Lifecycle + +Construct an owned bus or deliberately use the default. Retain registration IDs +for cleanup. Stop producers, invalidate registrations and settle entered +callbacks before destroying borrowed targets. `close()` invalidates and rejects +new work but does not wait for running callbacks. An independent queued error +sink must survive pending-ticket destruction even after its listener is removed. + +## Execution + +Immediate delivery is synchronous on dispatch's caller. Queued delivery owns +the work without invoking it inline, preserving each listener's FIFO stream. +Platform/simulation adapters follow those owners; copies of one worker-delivery +adapter share a serial stream. Independent listeners/targets have no global +completion order. Exceptions propagate immediately or reach the queued error +sink according to the selected delivery mode. + +## Integration + +An application-owned bus can notify simulation-owned listeners synchronously: + +```cpp +CE::Events::EventBus bus; +const CE::Events::EventChannel score_changed{"score-changed"}; +int displayed_score = 0; +auto registration = bus.register_listener(score_changed, [&](const int& value) { + displayed_score = value; +}); +bus.dispatch(score_changed, 10); +static_cast(bus.unregister_and_wait(registration)); +``` + +Here the caller supplies synchronization by performing all operations on one +owner. Choose a delivery adapter before registering when producers use other +threads. + +## Limits and further reading + +Channel names/RTTI form an in-process identity, not a serialization protocol. +The default bus does not provide automatic callback lifetime or UI ownership. +Read [event delivery and failure contracts](../runtime/event-delivery.md), +[thread dispatch](../runtime/thread-dispatch.md) and +[the bus interface](../../projects/engine/include/cheryl/core/subsystems/event-bus.h). diff --git a/docs/engine-systems/gui.md b/docs/engine-systems/gui.md new file mode 100644 index 00000000..61a233ad --- /dev/null +++ b/docs/engine-systems/gui.md @@ -0,0 +1,108 @@ +# GUI + +## Purpose + +GUI integrations connect toolkit-native widgets/documents to Engine input and +retained rendering. The application controls session lifetime, UI ownership, +focus, pointer selection and integration with game state. + +## Interface + +Types below are in `CE::UI` unless another namespace is shown. + +The [TGUI](../modules/tgui.md) and [RmlUi](../modules/rmlui.md) modules each own +their session, native authoring API and recording/publication types. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`TGUI::Session `, {cpp:class}`RmlUi::Session `
Own toolkit lifetime; supply copied dimensions, time and input, and manage explicit keyboard focus. [Session construction](../gui/index.md#construct-widgets-on-the-ui-owner); [routing](../gui/index.md#route-input-and-focus-explicitly). | +| {cpp:class}`TGUI::RecordedScene `, {cpp:class}`RmlUi::RecordedScene `
Retain owned CPU recordings without live toolkit objects crossing to graphics. [Update and publication](../development/ui-adapters.md#update-publication-and-shutdown). | +| {cpp:class}`TGUI::SceneUploader `, {cpp:class}`RmlUi::SceneUploader `
Transfer owned recordings to the provider owner and publish complete scenes. [Upload and cleanup](../gui/index.md#publish-and-clean-up). | +| {cpp:class}`TGUI::Scene `, {cpp:class}`RmlUi::Scene `
Append retained UI draw packets during frame preparation. [Retained publication](../development/ui-adapters.md#update-publication-and-shutdown); [GUI task](../getting-started/how-to/create-gui.md). | + +### Configuration and capabilities + +| Type or choice | Purpose and details | +| --- | --- | +| {cpp:struct}`TGUI::SessionOptions `, {cpp:struct}`RmlUi::SessionOptions ` | Set maximum texture size; TGUI also supplies explicit font scale. RmlUi uses native document styling. [Adapter ownership](../development/ui-adapters.md#module-and-application-ownership); [TGUI](../modules/tgui.md), [RmlUi](../modules/rmlui.md). | +| {cpp:struct}`TGUI::Capabilities `, {cpp:struct}`RmlUi::Capabilities ` | Report keyboard focus, committed text, clipboard, cursor and IME availability; RmlUi also reports clipping/effects support. [Unavailable services](../development/ui-adapters.md#routing-and-unavailable-services). | +| {cpp:enum}`CE::Input::Routing::KeyboardPolicy` and pointer selection | `Exclusive`/`PassThrough` controls keyboard gameplay. The application separately selects pointer recipients. [GUI routing](../gui/index.md#route-input-and-focus-explicitly). | +| {cpp:struct}`TGUI::Materials `, {cpp:struct}`RmlUi::Materials ` | Supply compatible layout, alpha, texture and projection contracts. [Bridge responsibilities](../development/ui-adapters.md#bridge-responsibilities). | +| {cpp:struct}`TGUI::WidgetLayout ` | Typed placement/resizing for native TGUI widgets; RmlUi uses native RML/RCSS layout. [Choose an interface](../gui/index.md#choose-an-interface); [TGUI layout declarations](../../projects/modules/ui/tgui/include/cheryl/ui/tgui/layout.h). | + +## Usage + +### Uses + +Sessions consume copied input, dimensions and time. Adapters share Engine +font/image operations while retaining toolkit authoring, atlas and effect +behavior; Runtime supplies lifecycle and frame boundaries. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Input::System`
{cpp:struct}`CE::Input::Events::Record`
{cpp:struct}`CE::Rendering::Resources::Provider`
{cpp:class}`CE::Rendering::Resources::Material`
{cpp:class}`CE::Text::FontRasterizer`
{cpp:class}`CE::Runtime::PlatformDispatcher::Submission`
{cpp:class}`CE::Rendering::PassWriter`
{cpp:struct}`CE::Platform::FramebufferSize` | [Input](input.md)
[Resources](resources.md)
[Text](text.md)
[Dispatch](dispatch.md)
[Rendering](rendering.md)
[Display](display.md)
[Runtime](runtime.md) | + +### Used by + +Menus, editors and interactive HUDs return intents to game state. Frame +preparation appends uploaded scenes without exposing live toolkit objects to +playback. + +| Types | Systems | +| --- | --- | +| [`DemoGame`](../../projects/apps/demo/src/demo-game.h)
[Application-owned UI views](../gui/index.md#construct-widgets-on-the-ui-owner) | [Rendering](rendering.md) | + +## Contracts + +Create the session before native toolkit objects and call them only on the UI +owner. Input must outlive its session; external widgets/fonts/textures/documents +must not outlive toolkit shutdown. Toolkit global state imposes session +coexistence limits described by each owner. + +Feed the complete ordered input batch once so modifier releases remain visible. +Keyboard focus and pointer selection are separate choices. Copied tick +dimensions determine layout; monitor scale or a live window query must not be +invented on simulation. Recording/upload must preserve earlier immutable +generations already retained by frames. + +## Lifecycle + +Prepare materials/upload helpers during platform-owned initialization. Create +the toolkit session on its UI/simulation owner, then native objects. Update input, +time and layout there, record CPU state, dispatch upload and retain the latest +complete scene. During shutdown, stop UI calls and release application-held +toolkit references before `close_after_quiescence()` and session destruction. +Keep accepted upload dependencies alive until runtime settles them. + +## Execution + +In concurrent runtime mode, UI mutation/recording belongs on simulation and +resource upload belongs on graphics. Sessions permit final platform teardown +after simulation has joined. Toolkit callbacks must update UI/simulation-owned +state or submit owned work, not directly perform native graphics operations. + +## Integration + +An application creates a native button on the TGUI session owner: + +```cpp +auto button = tgui::Button::create("Apply"); +button->onPress([this] { apply_requested_ = true; }); +session_->gui().add(button); +session_->request_keyboard_focus(); +``` + +The application retains `session_` and the callback target. It must also select +pointer delivery, feed records and publish the recorded scene. The +[GUI construction guide](../gui/index.md) explains those surrounding steps; +[the focused task](../getting-started/how-to/create-gui.md) connects them to a game. + +## Limits and further reading + +There is no universal Engine widget API or context-owned UI manager. Reported +capabilities define clipboard, cursor, text/focus and IME availability. Read the +[module owner guides](../modules/index.md), +[UI adapter contracts](../development/ui-adapters.md) and +[the maintained toolkit demo](../../projects/apps/demo/src/tgui-demo.cpp). diff --git a/docs/engine-systems/index.md b/docs/engine-systems/index.md new file mode 100644 index 00000000..94604497 --- /dev/null +++ b/docs/engine-systems/index.md @@ -0,0 +1,94 @@ +# Engine Systems + +Cheryl provides the execution, input, resource and rendering boundaries around an +application-owned game. Start here after [First Game](../getting-started/first-game.md) +when you need to understand how those boundaries fit together. The detailed topic +guides remain the authority for individual operations and failure cases. + +## Ownership and relationships + +The application owns its game and composes a compatible runtime context. The +context owns display, presentation, renderer and resource adapters; input can be +owned or explicitly borrowed. `GameRuntime` borrows the context and game for one +session. GUI sessions and audio systems have their own application-owned +lifetimes; they are composed alongside the runtime rather than installed into a +universal system manager. + +```text +Application + owns game state and application services + owns Context + owns display/window, presentation, renderer, resources + owns input or borrows an application-owned input system + owns tracked worker groups and lazy pool, or retains a supplied shared pool + owns GameRuntime, borrowing Context and Game + owns optional GUI session and audio system + +Input polls -> game updates -> immutable render frames -> rendering/presentation +Asset preparation -> resource upload/publication -> retained draw packets +Events -> immediate listeners or selected dispatch/worker delivery +``` + +Owning an object, borrowing an interface, retaining an immutable result and +submitting work carry different obligations. A retained draw packet keeps its +logical resources alive; it cannot keep a destroyed graphics backend usable. +A saved submission endpoint remains safe to call after closure but rejects new +work. A borrowed window still requires its owner thread and live display. + +## Execution + +| Owner | Work | +| --- | --- | +| Platform/graphics caller of `run()` | Poll input, operate window/graphics, initialize/clean up the game, upload, render and present. See [runtime ownership](runtime.md#execution). | +| Simulation | Update game state and prepare complete frames on platform or one dedicated simulation thread. See [frame publication](../runtime/runtime-frame-boundary.md#frame-publication-and-recycling). | +| Worker pool | Execute accepted CPU jobs independently of simulation, without graphics access. See [worker execution](workers.md#execution). | +| Audio implementation | Advance playback on its output clock, with separately synchronized controls/maintenance. See [audio execution](audio.md#execution). | + +Simulation reads copied tick input and dimensions. Rendering consumes a completed +frame instead of mutable game objects. Use [Dispatch](dispatch.md) when work must +reach a particular owner and [Workers](workers.md) for independent CPU capacity. +The [runtime lifecycle](runtime.md) supplies the shutdown order joining those +domains. + +## Reading order + +| System overview | Purpose and details | +| --- | --- | +| [Runtime](runtime.md) | Game hooks, context/session objects, execution and timing choices. [Frame lifecycle](../runtime/runtime-frame-boundary.md), [simulation timing](../runtime/simulation-timing.md). | +| [Display](display.md) | Windows, monitors, logical/pixel sizes and window modes. [Display/window ownership](../runtime/display-and-window-contract.md). | +| [Input](input.md) | Actions, keyboard/mouse/gamepad controls, capture, records and focus enums. [Control identities and tokens](../runtime/input-state-model.md#control-identities-and-binding-tokens), [input state model](../runtime/input-state-model.md). | +| [Assets](assets.md) | Loaders, caches, sprite/tile definitions and playback choices. [Loading](../assets/asset-loading.md), [manifests](../assets/asset-manifests.md), [values and playback](../assets/asset-values-and-playback.md). | +| [Resources](resources.md) | Providers, retained graphics handles and sampling choices. [Publication and retirement](../resources/resource-residency.md). | +| [Rendering](rendering.md) | Frames, cameras, draw packets, materials and fixed-state enums. [Pipelines, materials and submission](../rendering/pipelines-and-materials.md). | +| [Text](text.md) | Font selection, shaped/retained text, direction and rasterization choices. [Unicode layout](../assets/text-layout.md), [encoding and indices](../assets/text-encoding.md). | +| [GUI](gui.md) | Toolkit sessions, recordings, uploaders and reported capabilities. [GUI construction](../gui/index.md), [UI adapter contracts](../development/ui-adapters.md). | +| [Audio](audio.md) | Clips, voices, playback options and state. [Playback and ownership](../runtime/audio.md). | +| [Events](events.md) | Buses, channels, registrations and delivery choices. [Event delivery](../runtime/event-delivery.md). | +| [Dispatch](dispatch.md) | Owner queues, saved endpoints, futures and cancellation. [Thread dispatch](../runtime/thread-dispatch.md). | +| [Workers](workers.md) | Pools, groups, topology and CPU-policy enums. [Worker execution](../runtime/worker-execution.md). | +| [Memory](memory.md) | Blocks, pooled objects, reservations and release contexts. [CPU memory lifetime](../resources/resource-lifetime.md). | +| [State](state.md) | Sample tracking, synchronized publication, observation and machines. [State utility contracts](../runtime/utility-contracts.md#sampled-and-synchronized-state). | +| [Logging](logging.md) | Named loggers, severity, profiles and queue choices. [Logging configuration](../runtime/logging.md), [failure reporting](../runtime/failure-reporting.md). | +| [Diagnostics](diagnostics.md) | Copied statistics, domain IDs and observation boundaries. [Subsystem diagnostics](../runtime/subsystem-diagnostics.md). | +| [Utilities](utilities.md) | Time, bits, numeric parsing, file discovery and pointer helpers. [Utility contracts](../runtime/utility-contracts.md), [file discovery](../assets/file-and-font-discovery.md). | + +## Systems and modules + +These labels describe responsibilities. They do not prescribe CMake libraries, +manager singletons or one object per system. Engine defines portable contracts; +the [module guides](../modules/index.md) describe concrete implementations and +their selection. The [adapter contract catalogue](../architecture/interfaces-adapters.md) +lists required runtime interfaces, optional capabilities and cooperating +integrations, with obligations and detailed references for each. Start there +when replacing a backend; the [SDL3 scaffold](../development/modules.md#sdl3-scaffold) +establishes a source boundary for that future platform implementation. +[Ownership and Cleanup](../architecture/ownership-cleanup.md) explains retained, +borrowed and scoped lifetimes across systems; [Threads and Data Flow](../architecture/threads-data-flow.md) +helps choose the execution and publication path for work crossing those systems. +Native GLFW implements related display/input responsibilities; +OpenGL implements related rendering/resource responsibilities. Optional toolkit +and audio modules add capabilities without becoming reverse Engine dependencies. + +The [API Reference](../api/index.md) describes declarations. The subject guides +explain detailed contracts; [Create a...](../getting-started/create-a/index.md) +and [How to...](../getting-started/how-to/index.md) connect them to game code. diff --git a/docs/engine-systems/input.md b/docs/engine-systems/input.md new file mode 100644 index 00000000..658957dd --- /dev/null +++ b/docs/engine-systems/input.md @@ -0,0 +1,134 @@ +# Input + +## Purpose + +Input turns device observations into semantic actions and, when requested, +ordered physical/text records. Games consume an immutable tick view without +polling native devices or taking ownership of another consumer's records. + +## Interface + +Types below are in `CE::Input` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`System `
Device IDs, bindings, capture/focus support and complete poll publication. [Adapter publication](../runtime/input-state-model.md#adapter-publication); [adapter catalogue](../architecture/interfaces-adapters.md#runtime-adapters). | +| {cpp:class}`Actions::Bindings `, {cpp:struct}`Actions::Chord `, {cpp:struct}`Actions::AxisOptions `
Register buttons, simultaneous chords and scaled/gated axes on the platform owner. [Bindings](../runtime/input-state-model.md#bindings); [binding task](../getting-started/how-to/bind-input.md). | +| {cpp:struct}`Actions::Id `, {cpp:type}`Actions::BindingId `, {cpp:struct}`Control `
Distinguish game meaning, one registration and a physical device/control pair. [Control identities and tokens](../runtime/input-state-model.md#control-identities-and-binding-tokens). | +| {cpp:class}`Tick `
Read button/axis state, observed duration and ordered record views during update. [Tick input](../runtime/input-state-model.md#tick-input); [input and simulation time](../runtime/simulation-timing.md#input-and-tick-time). | +| {cpp:struct}`PollSnapshot `, {cpp:class}`Actions::Snapshot `
Retain complete immutable poll results across later polling. [Poll snapshots](../runtime/input-state-model.md#poll-snapshots); [ordered records](../runtime/input-state-model.md#ordered-events-and-os-text). | +| {cpp:class}`Capture::Lease `, {cpp:class}`Routing::Lease `
Keep a record channel requested or a particular keyboard-focus request alive. [Capture](../runtime/input-state-model.md#ordered-events-and-os-text); [routing](../runtime/input-state-model.md#focus-routing-and-editing-controls). | + +### Controls for bindings + +Pass these Engine enums directly with the corresponding device ID when binding +buttons, chords or axes. + +| Enum or constants | Purpose and details | +| --- | --- | +| {cpp:enum}`KeyboardKey ` | Letter/digit positions, editing/navigation keys, `F1`–`F25`, keypad and left/right modifiers; pair with `keyboard_id()`. [Identities, headers and binding examples](../runtime/input-state-model.md#control-identities-and-binding-tokens). | +| {cpp:enum}`MouseButton ` | `Left`, `Right`, `Middle`, `Extra1`–`Extra5`; pair with `mouse_id()`. [Mouse binding path](../runtime/input-state-model.md#control-identities-and-binding-tokens). | +| {cpp:enum}`GamepadControl ` | Named buttons, D-pad, sticks, triggers and optional sensor axes; pair with `gamepad_id()`. `A`/`B`/`X`/`Y` describe Xbox-style positions. Availability depends on the backend/device. [Names and limits](../runtime/input-state-model.md#control-identities-and-binding-tokens). | +| `MouseControl` | Absolute `X`/`Y`, one-poll `WheelUp`/`WheelDown` button pulses, and relative `DeltaX`, `DeltaY`, `ScrollX`, `ScrollY`, paired with `mouse_id()`. [Axis behavior](../runtime/input-state-model.md#ordered-events-and-os-text); [constant declarations](../../projects/engine/include/cheryl/core/controls/input-types.h). | +| {cpp:enum}`AxisKind ` | `Absolute` retains the final value; `Relative` accumulates changes. Set through `Actions::AxisOptions`. [Axis mapping and observation](../runtime/input-state-model.md#ordered-events-and-os-text). | + +Keyboard positions do not supply localized text. `Unknown` is not bindable; +standard enums are converted by `Control`, while adapters normalize native codes. +The [control guide](../runtime/input-state-model.md#control-identities-and-binding-tokens) +explains this distinction and lists the headers without repeating every key here. + +### Capture, records and routing values + +| Enum or value | Purpose and details | +| --- | --- | +| {cpp:enum}`Capture::Channel ` | `State` is always collected; `Events` requests physical records; `Text` requests committed Unicode scalars. [Channel activation and leases](../runtime/input-state-model.md#ordered-events-and-os-text). | +| {cpp:enum}`Routing::KeyboardPolicy ` | `Exclusive` gates keyboard gameplay; `PassThrough` also permits it. Focus does not activate capture. [Focus and routing](../runtime/input-state-model.md#focus-routing-and-editing-controls). | +| {cpp:enum}`DeviceKind `, {cpp:enum}`Events::ButtonPhase `, {cpp:enum}`Modifiers ` | Identify record source, press/release/repeat and modifier flags. [Record interpretation](../runtime/input-state-model.md#ordered-events-and-os-text). | +| {cpp:struct}`Events::Record `, {cpp:type}`Events::Payload ` | Ordered button, axis, pointer, scroll or committed-text payload with target/epoch metadata. [Record ownership](../runtime/input-state-model.md#ordered-events-and-os-text); [target filtering](../runtime/input-state-model.md#focus-routing-and-editing-controls). | +| {cpp:enum}`PollingPolicy `, {cpp:struct}`PollingOptions ` | `Lockstep`, `Finite`, `Unlimited` capacity and minimum spacing between completed polls. [Backlog and scheduling](../runtime/input-state-model.md#polling-backlog-and-scheduling). | + +## Usage + +### Uses + +A concrete adapter borrows a live window and translates device observations into +complete input polls. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Platform::Window`
{cpp:class}`CE::Input::Actions::Bindings`
{cpp:class}`CE::Input::Capture::Collector`
{cpp:class}`CE::Input::Routing::Manager` | [Display](display.md) | + +### Used by + +Runtime transfers polls into immutable tick views. Game updates read actions; +toolkit sessions read ordered records and request keyboard/text focus. Input +owns neither a simulation scheduler nor a widget tree. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Runtime::GameRuntime`
{cpp:struct}`CE::Game::TickContext`
{cpp:class}`CE::UI::TGUI::Session`
{cpp:class}`CE::UI::RmlUi::Session`
[`Tutorial::Player`](../getting-started/create-a/player.md) | [Runtime](runtime.md)
[GUI](gui.md) | + +## Contracts + +Bind and unbind controls on the platform owner. Button edges/counts and observed +hold durations describe the consumed polling interval; they do not schedule +updates or replace `tick.delta_seconds`. Relative axes accumulate changes, +whereas absolute axes retain their final value. `button_simulation_seconds()` +maps observed down-time proportion onto the selected simulation delta when that +is the desired movement policy. + +Records preserve collection order and committed text comes from the backend's +text input, not inferred key names. Readers may inspect the same record batch. +Capture activation and focus are latched at poll start; requests cannot +retroactively reroute already captured records. Exclusive keyboard focus gates +keyboard gameplay input while leaving other devices available. A superseded +focus lease cannot clear a newer request. + +## Lifecycle + +The context may own input or explicitly borrow it. Borrowed input must survive +runtime cleanup/context lifetime and detach from a still-live window. Register +bindings during game initialization, retain their IDs and remove them during +cleanup, including partial initialization. Keep capture/focus leases only while +their consumer needs them. Lease state and delivered immutable snapshots can +survive adapter destruction; they do not retain a native window. + +Shutdown discards pending, unpublished input after simulation stops. It does not +perform an extra game update to consume those records. Already delivered handles +remain valid. + +## Execution + +Polling, native callback collection, binding changes and publication run on the +platform owner. Runtime transfers complete batches to simulation. Update reads +the tick's borrowed view during that hook; copy owned data for longer use. +Capture and routing requests support concurrent callers, while delivered records +are read-only. GUI widgets are called on their UI/simulation owner, never from a +native callback. + +## Integration + +After binding application actions during `init()`, an update can select direction +and speed without referring to physical keys: + +```cpp +const float direction = (tick.input.button(move_right).held() ? 1.0f : 0.0f) + - (tick.input.button(move_left).held() ? 1.0f : 0.0f); +position.x += direction * speed * static_cast(tick.delta_seconds); +``` + +Here `position`, `speed` and the action IDs belong to the game. For a menu or +editor, route ordered records through a toolkit session instead of deriving text +from these actions. See [binding actions](../getting-started/how-to/bind-input.md) +and [GUI construction](../gui/index.md). + +## Limits and further reading + +Backend capabilities determine whether Events, committed Text and focus routing +are available. The GLFW backend does not promise composition/preedit, clipboard +editing or a complete IME/editor protocol. Read +[actions, snapshots, polling, records and focus](../runtime/input-state-model.md), +[timing](../runtime/simulation-timing.md) and +[the input interface](../../projects/engine/include/cheryl/core/controls/input-interface.h). diff --git a/docs/engine-systems/logging.md b/docs/engine-systems/logging.md new file mode 100644 index 00000000..4e9a41e5 --- /dev/null +++ b/docs/engine-systems/logging.md @@ -0,0 +1,102 @@ +# Logging + +## Purpose + +Logging records operational information in named destinations with explicit +configuration, filtering and sink lifetimes. Ordinary logs support diagnosis; +required failure propagation and bounded emergency reporting remain independent. + +## Interface + +Types below are in `CE::Logging` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`Logger `, {cpp:class}`Log `
Select a named category and initialize, emit, filter, flush, close or reopen it. [Runtime settings and ownership](../runtime/logging.md#runtime-settings-and-ownership). | +| {cpp:struct}`Config `
Choose destinations, rotation, levels and queue overflow behavior before producers start. [Configuration and ownership](../runtime/logging.md#runtime-settings-and-ownership). | +| {cpp:struct}`FileEventHandlers `
Supply scoped file-open/close callbacks while retaining borrowed targets through final closure. [Callback and reentry boundaries](../runtime/logging.md#filtering-and-failure-boundaries). | +| {cpp:struct}`QueueStats `
Observe queued/discarded work in the shared logging pool. [Queue observations](../runtime/logging.md#runtime-settings-and-ownership). | + +### Levels and policy enums + +| Type | Choices and details | +| --- | --- | +| {cpp:enum}`Level ` | `trace`, `debug`, `info`, `warn`, `err`, `critical`, `off` for runtime logger/sink thresholds. [Filtering](../runtime/logging.md#filtering-and-failure-boundaries). | +| {cpp:enum}`Profile ` | `Developer`, `Support`, `Release` presets through `Config::for_logger()`. [Runtime defaults](../runtime/logging.md#runtime-settings-and-ownership). | +| {cpp:enum}`OverflowPolicy ` | `Block` waits for queue capacity; `DiscardNew` rejects/counts new work without evicting accepted items. [Queue admission](../runtime/logging.md#runtime-settings-and-ownership). | +| {cpp:enum}`Compile::LogLevel ` | Compile-time severity bits used by logging policy; runtime thresholds cannot restore stripped calls. [Compile policy](../runtime/logging.md#compile-policy); [declarations](../../projects/engine/include/cheryl/core/logging/compile-policy.h). | + +## Usage + +### Uses + +Private backend machinery owns emission, rotation and console/file callbacks. +Build policy selects compiled severities; bounded failure reporting remains +available when ordinary logging is closed, stripped or degraded. + +| Types | Systems | +| --- | --- | +| {cpp:struct}`CE::Logging::Config`
{cpp:struct}`CE::Logging::FileEventHandlers`
{cpp:class}`CE::State::Singleton`
{cpp:type}`CE::Diagnostics::DomainId` | [State](state.md)
[Diagnostics](diagnostics.md) | + +### Used by + +Applications configure categories before producers and can add names. Category +state is keyed centrally by text across libraries. Explicit engine destinations +keep their selection; `make_default()` selects Engine's private registry +default. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Runtime::GameRuntime`
{cpp:class}`CE::Platform::GLFW::DisplaySystem`
{cpp:class}`CE::Rendering::OpenGL::Renderer`
{cpp:struct}`CE::Assets::Loading::Loader`
{cpp:struct}`CE::Memory::Manager`
{cpp:class}`CE::Workers::Pool` | [Runtime](runtime.md)
[Display](display.md)
[Rendering](rendering.md)
[Assets](assets.md)
[Memory](memory.md)
[Workers](workers.md)
[Diagnostics](diagnostics.md) | + +## Contracts + +Initialize explicitly before lazy access/writers when configuration matters. +Severity filters cannot restore code removed by compilation policy. Queue +admission, rotation and callback failures follow the configured contract; +logging is not a substitute for the operation's exception or future. + +Sink callbacks must respect backend reentry guards and their documented borrowed +arguments. Do not emit ordinary logging while holding sensitive subsystem locks +or from native/noexcept failure paths that require bounded emergency reporting. + +## Lifecycle + +Configure the used categories before starting producers, run those producers, +stop/settle them, then flush and close logging. Close/reopen serializes state +transitions; a timed close can report an incomplete operation rather than +pretending all queued work has completed. Keep callback targets alive until the +backend has finished with them. Startup composition owns its logging lifetime +when using the supplied startup path. + +## Execution + +Writers may enqueue from supported producer threads. Sink callbacks run according +to synchronous/asynchronous configuration, not necessarily on simulation. +Compile/filter checks should precede costly argument construction where the +selected frontend supports that. Named logger registry state is separate from +game/thread ownership. + +## Integration + +The application can emit through a built-in named category after startup: + +```cpp +CE::Logging::Logger::info("Game initialized"); +``` + +Select the declared category spelling from the +[logging umbrella](../../projects/engine/include/cheryl/core/logging.h) and +configure it before producers. The detailed guide shows custom names, handlers, +profiles and controlled shutdown. + +## Limits and further reading + +Logging does not promise recovery from a native failure or durable completion +merely because a write returned. Read +[configuration, emission and filtering](../runtime/logging.md), +[failure reporting](../runtime/failure-reporting.md), +[logging acceptance](../development/logging-acceptance.md) and +[the logger interface](../../projects/engine/include/cheryl/core/logging/logger.h). diff --git a/docs/engine-systems/memory.md b/docs/engine-systems/memory.md new file mode 100644 index 00000000..3ea911da --- /dev/null +++ b/docs/engine-systems/memory.md @@ -0,0 +1,104 @@ +# Memory + +## Purpose + +Memory provides CPU block bookkeeping, pooled object construction, reservations +and retained release contexts. Storage ownership and an object's live state are +separate; returning a block is not permission to destroy still-live objects. + +## Interface + +Types below are in `CE::Memory` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:struct}`Block `, {cpp:struct}`Manager `
Describe ranges and check out/return/coalesce CPU storage. [Pool ownership](../resources/resource-lifetime.md#pool-ownership); [block transactions](../resources/resource-lifetime.md#block-transactions). | +| {cpp:struct}`Objects::Pool `
Construct retained object handles whose release state can outlive the facade. [Object-pool ownership](../resources/resource-lifetime.md#pool-ownership). | +| {cpp:class}`Objects::Reservation `
Reserve slots and transfer individually constructed objects into handles. [Reservation and allocator requirements](../resources/resource-lifetime.md#reservations). | +| {cpp:struct}`Objects::Constructor CE::Memory::Objects::Constructor>`
Track construction phases while user constructors/destructors run outside bookkeeping locks. [Object construction](../resources/resource-lifetime.md#object-construction). | +| {cpp:struct}`ByteReleaseContext `
Retain byte bookkeeping for final release without a destroyed-manager lookup. [Release contexts](../resources/resource-lifetime.md#release-contexts). | + +### Storage and lifetime choices + +| Choice | Purpose and details | +| --- | --- | +| Pooled handles / reserved slots | Handles own claimed objects; a reservation returns only its unclaimed ranges. [Reservation lifetime](../resources/resource-lifetime.md#reservations). | +| {cpp:struct}`Allocators::ObjectPoolAllocator ` | Supplies the retained manager/release contract needed by reservations; ordinary allocators do not automatically qualify. [Supported allocators](../resources/resource-lifetime.md#reservations). | +| {cpp:enum}`FitPolicy ` | `exact`, `larger`, `greedy` select length-growth policy; these helpers do not allocate storage. [Fit arithmetic and limits](../runtime/utility-contracts.md#timing-and-numeric-helpers). | +| CPU / native resource ownership | CPU bookkeeping does not authorize native GPU use or replace graphics retirement. [CPU lifetime](../resources/resource-lifetime.md); [native retirement](../resources/resource-residency.md#native-retirement-and-maintenance). | + +## Usage + +### Uses + +Allocators/backing owners supply storage and construction contexts track live +slots. Synchronization protects bookkeeping; Diagnostics observes copied +allocation statistics. + +| Types | Systems | +| --- | --- | +| {cpp:struct}`CE::Memory::Allocators::ObjectPoolAllocator`
{cpp:struct}`CE::Memory::Objects::Constructor CE::Memory::Objects::Constructor>`
{cpp:struct}`CE::Memory::BlockTransactions`
{cpp:class}`CE::State::Singleton`
{cpp:type}`CE::Diagnostics::DomainId` | [State](state.md)
[Utilities](utilities.md)
[Diagnostics](diagnostics.md) | + +### Used by + +Caches and prepared geometry retain managed CPU backing. Application pools need +no runtime lifecycle. Resources has a separate native lifetime contract; +bookkeeping locks do not synchronize arbitrary object contents or authorize +graphics calls. + +| Types | Systems | +| --- | --- | +| {cpp:struct}`CE::Assets::Cache::Store`
{cpp:struct}`CE::Assets::Grids::GeometryData`
{cpp:struct}`CE::Text::Legacy::AsciiFont`
[Application-owned pooled objects](../resources/resource-lifetime.md#pool-ownership) | [Assets](assets.md)
[Text](text.md) | + +## Contracts + +Checkout/return preserves nonoverlapping ownership and contiguous partition +invariants. Construct/destroy operations on a busy slot reject. User constructors +and destructors run outside tracking locks. Allocation or batch construction may +fail after earlier independent work succeeds; owners must clean up completed +objects. Trusted destructor release is nonthrowing and terminates on broken +bookkeeping invariants. + +Block/pointer helpers do not validate every dereference or establish C++ object +lifetime. Callers remain responsible for live object access, alignment and +external quiescence where raw collection access is used. + +## Lifecycle + +Acquire storage with a live manager, construct objects, retain handles and let +their deleters destroy/return slots through captured release state. That state +can outlive the facade. A reservation returns unclaimed ranges; claimed handles +may survive it. Finish facade callers before destroying the facade even when +retained contexts permit later final release. + +## Execution + +Bookkeeping and distinct-slot construction support synchronized/reentrant +operations as documented. This does not synchronize members of a pooled game +object. Choose a simulation owner or add application synchronization. Native GPU +release continues to follow backend affinity/retirement rather than these CPU +manager locks. + +## Integration + +A class can allocate a batch of default-constructible application particles: + +```cpp +struct Particle { float age = 0; }; +auto particles = CE::Memory::Objects::Pool::get().retrieve_objects(16); +particles.front()->age = 0.25f; +particles.clear(); +``` + +Each shared handle retains the release context. Keep mutation on the game's +owner; sharing a handle alone does not make `age` synchronized. + +## Limits and further reading + +There is no general memory budget, automatic world ownership or NUMA placement +policy here. Read [CPU memory lifetime and reservation](../resources/resource-lifetime.md), +[value/pointer utility contracts](../runtime/utility-contracts.md), +[object pool declarations](../../projects/engine/include/cheryl/core/resources/objects/pool.h) +and [native resource lifetime](../resources/resource-residency.md). diff --git a/docs/engine-systems/rendering.md b/docs/engine-systems/rendering.md new file mode 100644 index 00000000..d5d2c7e2 --- /dev/null +++ b/docs/engine-systems/rendering.md @@ -0,0 +1,105 @@ +# Rendering + +## Purpose + +Rendering turns a completed snapshot of visual state into ordered backend draws +and presentation. The game selects what to show; the renderer consumes copied +values and retained resources without reading live simulation objects. + +## Interface + +Types below are in `CE::Rendering` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`FrameWriter `, {cpp:class}`PassWriter `, {cpp:class}`Frame `
Build ordered passes in a borrowed slot, then publish complete retained visual state. [Frame storage and writers](../rendering/pipelines-and-materials.md#frame-storage-and-writers); [handoff](../runtime/runtime-frame-boundary.md#frame-publication-and-recycling). | +| {cpp:struct}`DrawStyle2D `, {cpp:struct}`DrawPacket2D `
Resolve sprite/tile/graphic/text resources and copied placement into complete packets. [CPU submission](../rendering/pipelines-and-materials.md#cpu-submission). | +| {cpp:class}`Camera2D `, {cpp:class}`Camera3D `
Own camera state and provide matrices copied into a pass. [Camera observations](../rendering/pipelines-and-materials.md#camera-observations); [camera task](../getting-started/how-to/use-camera.md). | +| {cpp:class}`Resources::Pipeline `, {cpp:class}`Resources::Material `
Retain executable/fixed state and resolve typed material parameters. [Parameters and ownership](../rendering/pipelines-and-materials.md#parameters-and-ownership); [manual construction](../rendering/pipelines-and-materials.md#manual-material-construction). | +| {cpp:class}`Renderer `, {cpp:class}`PresentationSurface `
Consume completed frames and present through compatible graphics/platform adapters. [Adapter catalogue](../architecture/interfaces-adapters.md#runtime-adapters); [OpenGL owner](../modules/opengl.md). | + +### Draw values and enums + +| Type | Choices and details | +| --- | --- | +| {cpp:enum}`Pipelines::Blend ` | `Opaque`, `StraightAlpha`, `PremultipliedAlpha`, `Additive`; match source color/alpha convention. [Pipeline and material state](../rendering/pipelines-and-materials.md#parameters-and-ownership). | +| {cpp:enum}`Pipelines::Depth `, {cpp:enum}`Pipelines::Cull ` | Depth comparison and face culling; camera dimensionality does not select these policies. [Fixed state and pass constraints](../rendering/pipelines-and-materials.md#parameters-and-ownership). | +| {cpp:enum}`Geometry::Layout2D `, {cpp:enum}`Geometry::Topology ` | Match position/UV or colored vertices and triangles/triangle strips to the pipeline. [Geometry and independent binding](../rendering/pipelines-and-materials.md#independent-binding). | +| {cpp:enum}`Parameters::Type `, {cpp:enum}`Parameters::Semantic ` | Declare parameter value types and custom/pass/draw semantics. [Typed parameters](../rendering/pipelines-and-materials.md#parameters-and-ownership). | +| {cpp:struct}`ClipRegion2D ` | Logical rectangular clip retained with ordered draw state. [Rectangular clipping](../rendering/pipelines-and-materials.md#rectangular-clipping). | + +## Usage + +### Uses + +Applications resolve assets, text and retained UI scenes into complete packets. +Resource handles and copied dimensions provide the backend inputs. + +| Types | Systems | +| --- | --- | +| {cpp:struct}`CE::Assets::Sprites::Asset`
{cpp:struct}`CE::Assets::Tilesets::Asset`
{cpp:class}`CE::Text::Rendering::Generation`
{cpp:class}`CE::UI::TGUI::Scene`
{cpp:class}`CE::UI::RmlUi::Scene`
{cpp:class}`CE::Rendering::Resources::Material`
{cpp:struct}`CE::Rendering::Geometry::Buffer2D`
{cpp:struct}`CE::Platform::FramebufferSize` | [Assets](assets.md)
[Text](text.md)
[GUI](gui.md)
[Resources](resources.md)
[Display](display.md) | + +### Used by + +Game and UI preparation write retained passes; Runtime coordinates frame slots +and playback. Cameras and object transforms remain application-owned. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Runtime::GameRuntime`
{cpp:struct}`CE::Game::AbstractGame`
[`Tutorial::Player`](../getting-started/create-a/player.md)
[`Tutorial::Hud`](../getting-started/create-a/hud.md)
{cpp:class}`CE::UI::TGUI::Scene`
{cpp:class}`CE::UI::RmlUi::Scene` | [Runtime](runtime.md)
[GUI](gui.md) | + +## Contracts + +Frame/pass writers borrow a runtime-owned slot for one preparation call. Do not +retain them or issue native graphics operations from preparation. Draw packets +resolve resources, transforms and parameters before publication; playback does +not need the source game object or animation cursor. Pipelines supply complete +validated fixed state and material parameter bindings must match their contracts. + +Camera matrices are borrowed references until copied into a pass. Camera mutation +is unsynchronized. Frame preparation can be skipped under concurrent backpressure; +advance gameplay/animation during update rather than while drawing. + +## Lifecycle + +Create/upload resources during platform-owned initialization or dispatched +upload work. Update camera and authoritative state on simulation. Prepare, +publish, consume and recycle frames through runtime. Recycling drops retained +packet generations on graphics; game cleanup precedes backend closure. + +## Execution + +Update and frame preparation share the selected simulation owner. Native draw, +presentation and retirement maintenance run on graphics. Concurrent mode +transfers complete slots; it does not grant the renderer access to mutable +simulation members. The newest completed frame may supersede an older waiting +frame without skipping authoritative game updates. + +## Integration + +A preparation hook begins a pass with copied camera values: + +```cpp +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::Submission::ImageParameter2D{"image", 0} +}; +pass.add(CE::Rendering::Submission::resolve_sprite(*sprite_, cell_, style_, context)); +``` + +The application supplies the retained sprite, valid cell and matching material +in `style_`. See [First Game](../getting-started/first-game.md#prepare-a-frame-publish-the-actors-current-appearance) +for complete initialization, transform and cleanup wiring. + +## Limits and further reading + +Cameras do not choose a scene policy or synchronize game state. World/entity +organization belongs to the application. Read +[pipelines, materials and submission](../rendering/pipelines-and-materials.md), +[frame ownership](../runtime/runtime-frame-boundary.md), +[resource retirement](../resources/resource-residency.md), +[camera task guide](../getting-started/how-to/use-camera.md) and +[Frame interfaces](../../projects/engine/include/cheryl/core/rendering/render-frame.h). diff --git a/docs/engine-systems/resources.md b/docs/engine-systems/resources.md new file mode 100644 index 00000000..b0936e54 --- /dev/null +++ b/docs/engine-systems/resources.md @@ -0,0 +1,101 @@ +# Resources + +## Purpose + +Resources converts owned CPU descriptions into backend images, geometry, +programs, pipelines and materials. Retained handles let assets and completed +frames share immutable generations while the backend controls native retirement. + +## Interface + +Types below are in `CE::Rendering` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:struct}`Resources::Provider `
Create images/font atlases, upload geometry and link programs on the selected backend owner. [Immutable publication](../resources/resource-residency.md#immutable-resource-publication); [adapter capabilities](../architecture/interfaces-adapters.md#backend-capabilities-and-resource-implementations). | +| {cpp:struct}`CE::Assets::Images::Decoded`
Own top-to-bottom CPU RGBA pixels returned by `Images::decode(path/bytes)`, shared with toolkit loading. [CPU preparation and upload](../assets/asset-loading.md#runtime-entry-point); [decoded image declarations](../../projects/engine/include/cheryl/assets/resources/decoded-image.h). | +| {cpp:struct}`Resources::Image `, {cpp:struct}`Geometry::Buffer2D `
Retain immutable uploaded image/geometry generations. [Native retirement](../resources/resource-residency.md#native-retirement-and-maintenance). | +| {cpp:struct}`Resources::Program `, {cpp:class}`Resources::Pipeline `, {cpp:class}`Resources::Material `
Retain executable programs, validated fixed state and material recipes. [Pipelines and materials](../rendering/pipelines-and-materials.md). | +| {cpp:class}`Sampling::Sampler `, {cpp:struct}`CE::Assets::Shaders::Builder`
Use optional immutable sampling and shader/material recipe capabilities. [Sampling](../rendering/pipelines-and-materials.md#immutable-sampling); [shader recipes](../assets/asset-manifests.md#shadermaterial-definitions). | + +### Sampling and shader choices + +| Type | Choices and details | +| --- | --- | +| {cpp:struct}`Sampling::Options `, {cpp:enum}`Sampling::Filter ` | Independent minification/magnification with `Nearest` or `Linear` filtering. [Immutable sampling](../rendering/pipelines-and-materials.md#immutable-sampling). | +| {cpp:enum}`Sampling::MipmapFilter `, {cpp:enum}`Sampling::Wrap ` | Mipmap `None`/`Nearest`/`Linear`; UV `ClampToEdge`/`Repeat`/`MirroredRepeat`. [Sampling and UV behavior](../rendering/pipelines-and-materials.md#immutable-sampling). | +| {cpp:enum}`Sampling::Anisotropy ` | `Disabled` or `MaximumSupported`; an unsupported maximum resolves to isotropic filtering. [Effective sampling](../rendering/pipelines-and-materials.md#immutable-sampling). | +| {cpp:enum}`CE::Assets::Shaders::Stage`, {cpp:enum}`ShaderStage ` | Indexed recipes support `Vertex`/`Fragment`; the separate legacy enum classifies shader files and includes additional stages. [Shader definitions](../assets/asset-manifests.md#shadermaterial-definitions); [legacy stage declarations](../../projects/engine/include/cheryl/enums/shader-types.h). | + +## Usage + +### Uses + +The graphics implementation supplies a compatible live context and native +retirement domain. CPU data comes from asset, text or UI preparation; managed +CPU backing follows the separate Memory contract. + +| Types | Systems | +| --- | --- | +| {cpp:struct}`CE::Assets::Images::Decoded`
{cpp:struct}`CE::Rendering::Geometry::Vertex2D`
{cpp:class}`CE::Rendering::PresentationSurface`
{cpp:struct}`CE::Assets::Shaders::Builder`
{cpp:type}`CE::Diagnostics::DomainId` | [Assets](assets.md)
[Rendering](rendering.md)
[Diagnostics](diagnostics.md) | + +### Used by + +Assets, materials and frames retain exact resource generations. Applications can +call provider operations directly on the correct owner; Runtime preserves +backend lifetime through accepted work and frame recycling. + +| Types | Systems | +| --- | --- | +| {cpp:struct}`CE::Assets::Loading::Loader`
{cpp:class}`CE::Rendering::Resources::Material`
{cpp:struct}`CE::Rendering::DrawPacket2D`
{cpp:class}`CE::Text::Rendering::Generation`
{cpp:class}`CE::UI::TGUI::SceneUploader`
{cpp:class}`CE::UI::RmlUi::SceneUploader`
{cpp:class}`CE::Runtime::Context` | [Assets](assets.md)
[Rendering](rendering.md)
[Text](text.md)
[GUI](gui.md)
[Runtime](runtime.md) | + +## Contracts + +Uploads copy transient pixels/vertices before returning. Uploaded contents are +immutable: changed content requires another handle and explicit publication. +Provider ownership and current-context checks apply even when the C++ handle is +shared. A shared logical handle does not extend a closed native domain's usability. + +Managers support one active provider/loading-owner domain. Publication can be +incremental and resource retirement is deferred; neither a cache lookup nor a +handle destructor promises immediate allocation/deletion on the current thread. + +## Lifecycle + +Initialize rendering before uploading. Keep provider/context dependencies alive +through pending requests, simulation completion and frame recycling. Drop game +handles while the backend is still live. Native shutdown collects tracked +resources and closes the domain; surviving logical handles remain safe for +destruction/CPU inspection but cannot draw through it. Partial-failure cleanup +must close remaining native state without masking the original failure. + +## Execution + +CPU decoding/preparation can run away from graphics. Creation/upload and native +collection obey the selected backend's graphics-owner/current-context rules. +The OpenGL implementation queues retirement from final release and collects it +on graphics; final shared-pointer release alone performs no GL operation. + +## Integration + +On the platform owner after graphics initialization: + +```cpp +auto pixels = CE::Assets::Images::decode(image_path); +auto image = engine_.resources().create_image(pixels); +``` + +Both values own their contents. You may release/reuse `pixels` after the upload; +retain `image` through the material or frame that needs it. For costly decode, +prepare on a worker and send the owned result through platform dispatch instead. + +## Limits and further reading + +The portable provider does not promise generic in-place mutation, atomic asset +batches or residency budgets. Read the complete +[cache/publication/retirement contract](../resources/resource-residency.md), +[CPU lifetime guide](../resources/resource-lifetime.md), +[provider interface](../../projects/engine/include/cheryl/assets/resources/resource-provider.h) +and [OpenGL owner guide](../modules/opengl.md). diff --git a/docs/engine-systems/runtime.md b/docs/engine-systems/runtime.md new file mode 100644 index 00000000..24a1521a --- /dev/null +++ b/docs/engine-systems/runtime.md @@ -0,0 +1,119 @@ +# Runtime + +## Purpose + +Runtime coordinates a single game session: adapter initialization, input polling, +simulation scheduling, render-frame handoff, presentation and orderly shutdown. +It runs application-defined game hooks rather than prescribing entities, scenes +or a world model. + +## Interface + +Types below are in `CE::Runtime` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`Context `
Own compatible adapters and obtain borrowed accessors and tracked worker groups. [Adapter catalogue](../architecture/interfaces-adapters.md); [context ownership](../runtime/worker-execution.md#context-ownership). | +| {cpp:class}`GameRuntime `
Borrow a game/context for one session; block in `run()` and request shutdown with `stop()`. [Runtime frame boundary](../runtime/runtime-frame-boundary.md). | +| {cpp:struct}`CE::Game::AbstractGame`
Supply `init`, `update`, frame preparation, `quiesce` and `deinit` hooks. [Game hooks and ownership](../development/consuming-engine.md#game-hooks-and-ownership); [shutdown](../runtime/runtime-frame-boundary.md#shutdown). | +| {cpp:struct}`CE::Game::TickContext`
Read simulation delta, copied dimensions, immutable input and the session stop request. [Input and owner threads](../runtime/runtime-frame-boundary.md#input-and-owner-threads); [tick time](../runtime/simulation-timing.md#input-and-tick-time). | +| {cpp:class}`CE::Application::Startup`
Configure the supplied command-line composition path before creating the runtime. [Command-line startup](../development/consuming-engine.md#command-line-startup); [Startup module](../modules/startup.md). | + +### Configuration and enums + +| Type | Choices and details | +| --- | --- | +| {cpp:enum}`RunMode ` | `Sequential` updates on platform; `Concurrent` uses a dedicated simulation thread. [Execution and frame handoff](../runtime/runtime-frame-boundary.md). | +| {cpp:struct}`ExecutionOptions ` | Lazy owned worker capacity or an application-supplied shared pool. [Context worker ownership](../runtime/worker-execution.md#context-ownership). | +| {cpp:struct}`Simulation::TimingOptions `, {cpp:enum}`Simulation::Mode ` | Configure variable/fixed updates, interval and bounded recovery. [Timing defaults](../runtime/simulation-timing.md#defaults-and-pacing); [fixed steps](../runtime/simulation-timing.md#fixed-steps). | +| {cpp:enum}`Simulation::LagRecovery `, {cpp:enum}`Simulation::UpdateKind ` | Select dropped excess lag or variable catch-up; identify the kind of the current tick. [Catch-up recovery](../runtime/simulation-timing.md#catch-up-recovery). | +| {cpp:struct}`CE::Input::PollingOptions`, {cpp:enum}`CE::Input::PollingPolicy` | Configure completed-poll backlog and spacing independently of simulation mode. [Polling backlog](../runtime/input-state-model.md#polling-backlog-and-scheduling). | + +## Usage + +### Uses + +Runtime composes the adapters and coordinates their owners, accepted work and +shutdown. + +| Types | Systems | +| --- | --- | +| {cpp:struct}`CE::Game::AbstractGame`
{cpp:class}`CE::Platform::DisplaySystem`
{cpp:class}`CE::Input::System`
{cpp:class}`CE::Rendering::PresentationSurface`
{cpp:class}`CE::Rendering::Renderer`
{cpp:struct}`CE::Rendering::Resources::Provider`
{cpp:class}`CE::Workers::Group`
{cpp:class}`CE::Runtime::PlatformDispatcher` | [Display](display.md)
[Input](input.md)
[Rendering](rendering.md)
[Resources](resources.md)
[Dispatch](dispatch.md)
[Workers](workers.md)
[Logging](logging.md)
[Diagnostics](diagnostics.md) | + +### Used by + +Applications construct a runtime and provide game hooks. GUI and audio +integrations cooperate with those lifecycle boundaries while keeping +application-owned lifetimes. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Application::Startup`
{cpp:struct}`CE::Application::StartupResult`
[`Tutorial::Game`](../getting-started/create-a/player.md) | [GUI](gui.md)
[Audio](audio.md) | + +## Contracts + +A context and runtime each support one session. Borrowed game/context/input +objects must survive all use and cleanup. `init()` may upload resources and +register bindings; `update()` and frame preparation must not perform graphics +operations when simulation is concurrent. A borrowed tick, frame writer or pass +writer is valid only for its hook call. + +Input observation time and simulation delta have distinct meanings. Rendering +sees complete published snapshots. Frame preparation may be skipped when no +slot is free, so it must not advance essential game state. Hook failures stop +the session; cleanup preserves the first failure while reporting later ones. + +## Lifecycle + +The application constructs adapters/context before running. Runtime initializes +input and graphics, attempts game initialization, then starts the selected +simulation execution. Game cleanup must tolerate partial initialization. + +Shutdown stops new context-group submissions and simulation, servicing accepted +platform dependencies while joining. `quiesce()` then stops external producers +and invalidates borrowed registrations without destroying dependencies still +needed by accepted work. After that work settles and retained frames recycle, +`deinit()` releases game resources while adapters remain alive. Input detaches +before its window disappears; graphics then closes. An injected worker pool's +unrelated groups remain usable. + +## Execution + +The caller of `run()` owns platform polling and graphics operations. Sequential +mode also updates/prepares on that thread using one frame. Concurrent mode gives +updates and preparation to one simulation thread and uses three frame slots. +Tick input and window/framebuffer dimensions cross as values; accessors do not +marshal live adapter operations onto their required thread. + +## Integration + +A game requests graceful shutdown from its update hook: + +```cpp +void update(const CE::Game::TickContext& tick) override { + if (tick.input.button(quit_action).pressed()) { + tick.request_stop(); + return; + } + // Advance application-owned state using tick.delta_seconds. +} +``` + +`quit_action` is an application-defined action registered during initialization. +The stop request does not interrupt the current hook; return when further work +is unnecessary. [First Game](../getting-started/first-game.md) shows the complete +main function and hook wiring. + +## Limits and further reading + +There is no built-in world, entity, physics or scene lifecycle. The runtime does +not automatically own toolkit sessions or audio. Read the detailed +[frame and shutdown contract](../runtime/runtime-frame-boundary.md), +[simulation timing](../runtime/simulation-timing.md), +[runtime architecture](../architecture/assembling-engine.md) and +[failure reporting](../runtime/failure-reporting.md). Public declarations live in +[Context](../../projects/engine/include/cheryl/core/engine/engine-context.h), +[GameRuntime](../../projects/engine/include/cheryl/core/game-framework/game-runtime.h) +and [AbstractGame](../../projects/engine/include/cheryl/core/game-framework/abstract-game.h). diff --git a/docs/engine-systems/state.md b/docs/engine-systems/state.md new file mode 100644 index 00000000..99b5418d --- /dev/null +++ b/docs/engine-systems/state.md @@ -0,0 +1,106 @@ +# State + +## Purpose + +State supplies reusable sampled values, versioned publication, observation and +transition tables. Applications choose when to sample or transition; these +types do not create threads or automatically synchronize an entire game. + +## Interface + +Types below are in `CE::State` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`StateTracker `
Retain previous/current samples and observe `changed()`/`delta()`. [Sampled state](../runtime/utility-contracts.md#sampled-and-synchronized-state). | +| {cpp:class}`VersionedVariable `
Publish and copy a synchronized value/revision pair; wait for a changed revision. [Synchronized publication](../runtime/utility-contracts.md#sampled-and-synchronized-state). | +| {cpp:class}`ObservedVariable `
Add immediate observers outside storage locks, on each setter's thread. [Observation and callbacks](../runtime/utility-contracts.md#sampled-and-synchronized-state). | +| {cpp:class}`StateMachine `
Store ordered guarded transitions and enter/exit/action hooks. [Transition tables](../runtime/utility-contracts.md#transition-tables). | +| {cpp:class}`Singleton `, {cpp:class}`FriendSingleton `
Deliberately share construction where an existing service contract requires it. [Singleton declarations and lifetime](../../projects/engine/include/cheryl/templates/singleton.h). | + +### Choosing a state type + +| Need | Purpose and details | +| --- | --- | +| Compare successive samples | Use `StateTracker` with caller synchronization; `delta()` uses T's units. [Sampling and assignment](../runtime/utility-contracts.md#sampled-and-synchronized-state). | +| Share the latest value | Use `VersionedVariable`; waits coalesce updates and are not event history. [Value/revision snapshots](../runtime/utility-contracts.md#sampled-and-synchronized-state). | +| React to setters | Use `ObservedVariable`; equal successful sets still notify and callbacks protect their own targets. [Observer execution](../runtime/utility-contracts.md#sampled-and-synchronized-state). | +| Model behavior transitions | Supply application-defined `State` and `Trigger` types, often enums, to `StateMachine`. No engine-wide enum is prescribed. [Rules, ordering and failure](../runtime/utility-contracts.md#transition-tables). | + +## Usage + +### Uses + +The supplied value types define comparison, copying, assignment and pointee +ownership. Callback captures retain their own dependencies; local state +utilities require no runtime, graphics, input or worker service. + +| Types | Systems | +| --- | --- | +| [Caller-defined `T`](../runtime/utility-contracts.md#sampled-and-synchronized-state)
[Caller-defined `State` and `Trigger`](../runtime/utility-contracts.md#transition-tables)
[`std::mutex` / `std::condition_variable`](../runtime/utility-contracts.md#sampled-and-synchronized-state)
[`std::function`](../runtime/utility-contracts.md#transition-tables) | — | + +### Used by + +Existing services use deliberate singleton construction. Games can separately +choose sampled values, synchronized publication, observers or transition +machines; the engine prescribes no application state-machine architecture. + +| Types | Systems | +| --- | --- | +| {cpp:struct}`CE::Assets::Loading::Loader`
{cpp:struct}`CE::Events::EventSystem`
{cpp:struct}`CE::Memory::Objects::Pool`
{cpp:class}`CE::Logging::Log`
[Application-owned state models](../runtime/utility-contracts.md#transition-tables) | [Assets](assets.md)
[Events](events.md)
[Memory](memory.md)
[Logging](logging.md) | + +## Contracts + +`snapshot()` copies a versioned value and revision together; separate getters may +observe different updates. Equal sets do not advance a versioned revision. +Waits coalesce updates and deliver current state rather than every intermediate +value. Assignment/copy exceptions retain the supplied type's guarantee, not a +transactional rollback guarantee. + +Observed callbacks run outside storage locks on the setter's thread, including +equal successful sets. Concurrent/reentrant setters can overlap callbacks. +State-machine guards/rules have insertion order; failures stop transition steps +without undoing prior callback effects. Do not recursively trigger the same +machine. + +## Lifecycle + +Own each value/machine within its application component. Keep borrowed callback +targets alive through calls and capture destruction. Versioned waiters require +a live variable and a producer able to make progress; destruction is not +cancellation. Borrowed accessor references expire with their owner or mutation +according to that type's contract. + +## Execution + +`StateTracker` and transition machines require caller synchronization. +Versioned storage synchronizes its value/revision, while callbacks protect their +own external targets. Select a [Dispatch](dispatch.md) owner when an observer +must change simulation state from another producer. + +## Integration + +Sample an application-owned score during update: + +```cpp +CE::State::StateTracker score{0}; +score.update(10); +if (score.changed()) { + const int gained = score.delta(); + // Use gained while preparing the next HUD content update. + static_cast(gained); +} +``` + +The tracker owns integer samples and introduces no clock. Keep it as a member +to compare successive updates rather than reconstructing it every tick. + +## Limits and further reading + +State publication is not event history, a completion barrier or automatic +thread assignment. Read [state and utility contracts](../runtime/utility-contracts.md), +[StateTracker](../../projects/engine/include/cheryl/templates/state-tracker.h), +[VersionedVariable](../../projects/engine/include/cheryl/templates/versioned-variable.h) +and [StateMachine](../../projects/engine/include/cheryl/templates/state-machine.h). diff --git a/docs/engine-systems/text.md b/docs/engine-systems/text.md new file mode 100644 index 00000000..0af5297c --- /dev/null +++ b/docs/engine-systems/text.md @@ -0,0 +1,111 @@ +# Text + +## Purpose + +Text selects font data, validates/decodes text, shapes Unicode runs and prepares +drawable glyphs. Font loading, shaping, rasterization and graphics upload are +different stages with different ownership and execution requirements. + +## Interface + +Types below are in `CE::Text` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:struct}`FontSelection `, {cpp:class}`FontCollection `
Select ordered fallback fonts and retain immutable font-byte snapshots. [Font selection](../assets/text-layout.md#font-selection); [font discovery](../assets/file-and-font-discovery.md). | +| {cpp:class}`ShapedText `, {cpp:struct}`LayoutOptions `
Run `layout_text()` and inspect shaped lines, glyphs and source mapping. [Shaping, direction and width](../assets/text-layout.md#shaping-direction-and-width). | +| {cpp:class}`Rendering::Prepared `, {cpp:class}`Rendering::Generation `
Prepare CPU glyph geometry, upload retained resources and resolve text draw packets. [Preparation and submission](../assets/text-layout.md#preparation-upload-and-retained-submission); [text task](../getting-started/how-to/update-text.md). | +| {cpp:class}`FontRasterizer `, {cpp:struct}`FontRasterGlyph `
Obtain shared CPU font metrics, raster pixels and kerning for toolkit adapters. [UI font responsibilities](../development/ui-adapters.md#bridge-responsibilities); [rasterizer declarations](../../projects/engine/include/cheryl/text/font-rasterizer.h). | +| {cpp:struct}`Legacy::Font `
Use the separate legacy ASCII/bitmap compatibility path. [Legacy bitmap fonts](../resources/legacy-ffont.md). | + +### Sources, direction and raster values + +| Type | Choices and details | +| --- | --- | +| {cpp:type}`FontSource ` | File, system family or string source selection; make discovery policy explicit. [Font sources and fallback](../assets/text-layout.md#font-selection). | +| {cpp:enum}`ParagraphDirection ` | `Automatic`, `LeftToRight`, `RightToLeft` paragraph direction. [Shaping and bidi direction](../assets/text-layout.md#shaping-direction-and-width). | +| {cpp:struct}`SourceRange `, {cpp:struct}`GlyphId ` | Distinguish source byte/scalar ranges from font-qualified glyph indices. [Encoding and source indices](../assets/text-encoding.md). | +| {cpp:enum}`FontSizing `, {cpp:enum}`FontBitmapFormat ` | Raster sizing in `Pixels`/`Characters`; output `Alpha`/`PremultipliedRGBA` pixel storage. [Raster size/format declarations](../../projects/engine/include/cheryl/text/font-rasterizer.h); [toolkit alpha conventions](../development/ui-adapters.md#bridge-responsibilities). | + +## Usage + +### Uses + +Font discovery and CPU processing precede resource upload and draw resolution. +Applications can prepare on Workers and dispatch owned upload requests to +graphics. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Text::FontCollection`
{cpp:class}`CE::Text::FontRasterizer`
{cpp:struct}`CE::Rendering::Resources::Provider`
{cpp:class}`CE::Rendering::Resources::Material`
{cpp:class}`CE::Runtime::PlatformDispatcher::Submission` | [Assets](assets.md)
[Utilities](utilities.md)
[Resources](resources.md)
[Rendering](rendering.md)
[Workers](workers.md)
[Dispatch](dispatch.md) | + +### Used by + +Games draw labels, dialogue and HUD text. Toolkit adapters use font +metrics/rasterization within their own layout; the application selects which +text path to use. + +| Types | Systems | +| --- | --- | +| [`Tutorial::Hud`](../getting-started/create-a/hud.md)
{cpp:class}`CE::UI::TGUI::Session`
{cpp:class}`CE::UI::RmlUi::Session` | [Rendering](rendering.md)
[GUI](gui.md) | + +## Contracts + +Layout positions use local Y-up pixels; `pixel_height` is the font em size. +Glyph IDs are qualified by their retained font collection. Shaping clusters and +UTF-8 byte/scalar ranges are not interchangeable character indices. Layout +retains the font-byte snapshot used for later preparation, not a borrowed input +string or mutable native face. + +Fallback and wrapping operate on the documented Unicode boundaries. Replacing +text produces another layout/uploaded generation; it must not mutate a +generation already retained by a frame. Select compatible text materials and +font resources before publishing. + +`FontRasterizer::load()` copies font bytes; its copies share synchronized native +face state. Returned metrics/glyph pixels own their values and can survive the +rasterizer. This CPU service neither borrows a graphics provider nor makes a +toolkit's mutable atlas thread-safe. + +## Lifecycle + +Load selected fonts at a loading boundary, lay out text and prepare its CPU +representation. Upload after graphics initialization and retain the generation +in the game/HUD. Replace it when content changes, allowing published frames to +retain older generations. Settle upload requests and release game handles before +backend shutdown. Retained font bytes and native graphics lifetime are separate. + +## Execution + +Font selection/layout/preparation are CPU stages; upload follows graphics +affinity. Use owned request data across dispatch boundaries and consume completed +futures without blocking simulation. Record positions and immutable generation +handles during frame preparation. + +## Integration + +With a previously loaded `fonts` collection on the platform upload owner: + +```cpp +auto layout = CE::Text::layout_text(fonts, "Score: 0", {.pixel_height = 24}); +auto prepared = CE::Text::Rendering::prepare(std::move(layout)); +auto score = CE::Text::Rendering::upload(std::move(prepared), engine_.resources()); +``` + +Retain `score`, resolve it with a compatible material in frame preparation and +replace it through the same ownership stages when the score changes. See +[the text task guide](../getting-started/how-to/update-text.md). + +## Limits and further reading + +Text rendering is not a full editor/IME service. Do not infer arbitrary toolkit +font compatibility or complete script acceptance from a basic label example. +Read [Unicode layout](../assets/text-layout.md), +[encoding and source indices](../assets/text-encoding.md), +[font discovery](../assets/file-and-font-discovery.md), +[legacy fonts](../resources/legacy-ffont.md), +[layout declarations](../../projects/engine/include/cheryl/text/layout.h), +[shared rasterization operations](../../projects/engine/include/cheryl/text/font-rasterizer.h) and +[text rendering declarations](../../projects/engine/include/cheryl/assets/types/2d/unicode-text.h). diff --git a/docs/engine-systems/utilities.md b/docs/engine-systems/utilities.md new file mode 100644 index 00000000..5348efcb --- /dev/null +++ b/docs/engine-systems/utilities.md @@ -0,0 +1,101 @@ +# Utilities + +## Purpose + +Time, math, numeric parsing and file helpers supply local value operations. +Their namespaces describe responsibility; they do not imply a shared service, +singleton manager or engine-wide initialization/shutdown phase. + +## Interface + +Types below are in `CE` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`Time::DeltaTime ` and duration helpers
Measure local wall-clock intervals and convert duration units. [Timing helpers](../runtime/utility-contracts.md#timing-and-numeric-helpers). | +| {cpp:class}`Math::BitArray `
Own packed bits with borrowed proxies/native word spans. [Packed bit representation](../runtime/utility-contracts.md#packed-bits-addresses-and-geometry). | +| `CE::Text::Numbers` parsing functions
Parse floating/signed/unsigned numeric text with explicit syntax and range failures. [Numeric parsing](../runtime/utility-contracts.md#timing-and-numeric-helpers); [declarations](../../projects/engine/include/cheryl/math/string-numbers.h). | +| {cpp:class}`Files::FileIndex `
Own normalized file-discovery results without automatic refresh. [File index](../assets/file-and-font-discovery.md#file-index). | +| `CE::Memory::Pointers` and fit/byte helpers
Calculate addresses/alignment, growth and byte formatting without owning storage. [Address preconditions](../runtime/utility-contracts.md#packed-bits-addresses-and-geometry); [fit arithmetic](../runtime/utility-contracts.md#timing-and-numeric-helpers). | + +### Values and selection rules + +| Choice | Purpose and details | +| --- | --- | +| Duration aliases / casts | Standard duration truncation and representability rules; no scheduling policy. [Time units](../runtime/utility-contracts.md#timing-and-numeric-helpers). | +| {cpp:enum}`Memory::FitPolicy ` | `exact`, `larger`, `greedy` choose length adjustment independently of managed allocation. [Growth rules](../runtime/utility-contracts.md#timing-and-numeric-helpers). | +| {cpp:enum}`Rendering::Geometry::AnchorType `, {cpp:struct}`Rendering::Geometry::Pivot ` | Choose named CPU quad placement or a normalized pivot; texture dimensions have explicit preconditions. [Anchor geometry](../runtime/utility-contracts.md#packed-bits-addresses-and-geometry); [coordinate conventions](../assets/asset-values-and-playback.md#coordinates-and-grid-geometry). | +| Local timer / runtime delta | Measure CPU work with `DeltaTime`; advance game simulation with the tick's selected delta. [Local timing](../runtime/utility-contracts.md#timing-and-numeric-helpers); [simulation time](../runtime/simulation-timing.md#input-and-tick-time). | + +## Usage + +### Uses + +Standard clocks, filesystem/numeric operations and containers supply local +values. Template arguments and caller-provided storage determine additional +ownership; ordinary helpers need no runtime, graphics or worker service. + +| Types | Systems | +| --- | --- | +| [`std::chrono::steady_clock` / durations](../runtime/utility-contracts.md#timing-and-numeric-helpers)
[`std::filesystem::path`](../assets/file-and-font-discovery.md#file-index)
[Caller-supplied spans / pointers](../runtime/utility-contracts.md#packed-bits-addresses-and-geometry) | — | + +### Used by + +Applications use local bookkeeping and preparation helpers. Assets discovers +files, Memory uses address/fit operations, and text/rendering preparation uses +CPU geometry. Runtime simulation timing keeps its own policy. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Assets::Loading::FileRegistry`
{cpp:struct}`CE::Memory::Manager`
{cpp:struct}`CE::Text::Legacy::BitmapFont`
[Application-owned loading tools](../assets/file-and-font-discovery.md#file-index) | [Assets](assets.md)
[Memory](memory.md)
[Text](text.md)
[Rendering](rendering.md) | + +## Contracts + +Duration conversions follow standard truncation/representability rules. Pointer +helpers do not allocate or prove storage bounds/object lifetime; supply valid +alignment and representable addresses. Packed bit access requires valid indices +and synchronization, even for different bits sharing a word. Native word layout +is not a portable serialized format. + +Numeric parsing has distinct floating/integer syntax and failure rules. File +index references borrow its container storage and do not refresh automatically. +Use each helper's actual contract rather than assuming a universal checked-value +or service-lifecycle guarantee. + +## Lifecycle + +Construct timers, indices and bit arrays as application-owned values. Keep their +borrowed references/spans within the owner's lifetime and invalidation rules. +Caller-supplied pointer ranges and pointees remain the caller's responsibility. +There is no runtime shutdown hook for a stateless conversion function. + +## Execution + +These local values generally require caller synchronization. A custom timer +clock defines backward-jump behavior; the default steady clock is monotonic. +Filesystem discovery may block and should run at a loading boundary or on an +appropriate worker, with owned results transferred to consumers. + +## Integration + +A standalone loading tool can measure one CPU preparation interval: + +```cpp +CE::Time::DeltaTime timer; +auto prepared = loader.prepare(); +const double seconds = timer(); +``` + +The loader owns the prepared result; the timer owns only its last clock sample. +In a game update, use `tick.delta_seconds` for runtime-selected simulation time +instead of substituting this independent wall-clock measurement. + +## Limits and further reading + +Helpers do not provide scheduling, automatic thread safety, portable pointer +identity or transactional behavior for arbitrary template types. Read the +[complete state/time/math/file contracts](../runtime/utility-contracts.md), +[file discovery guide](../assets/file-and-font-discovery.md) and +[DeltaTime header](../../projects/engine/include/cheryl/templates/delta.h). diff --git a/docs/engine-systems/workers.md b/docs/engine-systems/workers.md new file mode 100644 index 00000000..10800af3 --- /dev/null +++ b/docs/engine-systems/workers.md @@ -0,0 +1,100 @@ +# Workers + +## Purpose + +Workers shares CPU execution capacity between workload groups. Groups define +admission, concurrency, relative scheduling share and optional CPU/locality +policy without creating another physical pool per workload. + +## Interface + +Types below are in `CE::Workers` unless another namespace is shown. + +### Types and operations + +| API and purpose | +| --- | +| {cpp:class}`Pool `, {cpp:struct}`PoolOptions `
Own reusable CPU capacity, optionally informed by discovered topology. [Pool lifetime](../runtime/worker-execution.md#pool-lifetime); [capacity](../runtime/worker-execution.md#topology-providers-and-capacity). | +| {cpp:class}`Group `, {cpp:struct}`GroupOptions `
Submit owned jobs with concurrency caps, scheduling share and CPU policy; close then drain. [Jobs](../runtime/worker-execution.md#jobs); [CPU policy](../runtime/worker-execution.md#cpu-policy-and-capabilities). | +| {cpp:class}`TopologyProvider `, {cpp:struct}`Topology `
Supply/inspect native CPU, core and cache observations. [Topology providers](../runtime/worker-execution.md#topology-providers-and-capacity); [adapter catalogue](../architecture/interfaces-adapters.md#backend-capabilities-and-resource-implementations). | +| {cpp:struct}`Configuration `
Validate typed pool/group configuration; optional Workers integration loads JSON. [Typed and JSON configuration](../runtime/worker-execution.md#typed-and-json-configuration); [Workers module](../modules/workers.md). | +| {cpp:struct}`GroupPolicy `, {cpp:struct}`GroupStatus `, {cpp:struct}`Capabilities `
Compare requested/effective policy, activity and supported native behavior. [CPU policy](../runtime/worker-execution.md#cpu-policy-and-capabilities); [worker statistics](../runtime/subsystem-diagnostics.md#worker-statistics). | + +### CPU policy enums + +| Type | Choices and details | +| --- | --- | +| {cpp:struct}`CpuPolicy ` | Explicit eligible CPU IDs, strength, locality and optional cache-domain selection. [CPU policy and capabilities](../runtime/worker-execution.md#cpu-policy-and-capabilities). | +| {cpp:enum}`PolicyStrength ` | `Preferred` may report fallback; unsupported `Required` policy rejects. [Required and preferred policy](../runtime/worker-execution.md#cpu-policy-and-capabilities). | +| {cpp:enum}`LocalityPolicy ` | `Automatic`, `Reuse`, `Spread` within reported topology and binding support. [Locality selection](../runtime/worker-execution.md#cpu-policy-and-capabilities). | +| {cpp:enum}`CacheType ` | `Data`, `Instruction`, `Unified` classify discovered cache domains. [Topology snapshots](../runtime/worker-execution.md#topology-providers-and-capacity). | + +## Usage + +### Uses + +Optional native providers supply CPU/core/cache observations and affinity +enforcement. Jobs with owner-affine follow-up can request Dispatch; the pool +itself grants CPU capacity. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Workers::TopologyProvider`
{cpp:struct}`CE::Workers::Topology`
{cpp:struct}`CE::Workers::CpuPolicy`
{cpp:type}`CE::Diagnostics::DomainId` | [Diagnostics](diagnostics.md)
[Logging](logging.md)
[Dispatch](dispatch.md) | + +### Used by + +Runtime may own lazy capacity or retain a supplied shared pool. Applications can +schedule asset/text/audio preparation, and event delivery can use a serial +worker stream. Standalone services need no runtime session. + +| Types | Systems | +| --- | --- | +| {cpp:class}`CE::Runtime::Context`
{cpp:type}`CE::Events::EventBus::Delivery`
[Application-owned job callables](../runtime/worker-execution.md#jobs) | [Runtime](runtime.md)
[Assets](assets.md)
[Text](text.md)
[Events](events.md)
[Audio](audio.md) | + +## Contracts + +Jobs own callables and settle futures with results/errors. FIFO selection does +not imply completion order when concurrency exceeds one. Caps limit running +jobs; weight/priority determine selection share rather than OS scheduling +priority. CPU eligibility is not reserved core ownership or guaranteed speedup. + +Do not block a job on child futures from the same saturated pool. Close before +drain; drainage waits for accepted work and capture release. Same-pool drainage +and self-join reject. Required unsupported native policy rejects while preferred +policy may report fallback. + +## Lifecycle + +Create a pool and groups, submit owned work, close acceptance and drain before +destroying borrowed dependencies. Pool destruction closes, drains and joins; +saved group handles reject safely after pool disappearance. Thread-start failure +joins threads already created. Context-created groups close with that context; +unrelated groups in an injected shared pool remain open. + +## Execution + +Physical workers execute eligible groups according to scheduling policy. They +are separate from runtime's dedicated concurrent simulation thread. Graphics +work still needs platform dispatch. During runtime shutdown, accepted CPU work +settles while platform dependencies can continue making progress. + +## Integration + +During game initialization, retain a context-owned preparation group: + +```cpp +auto preparation = engine_.make_worker_group({.max_concurrency = 2}); +auto result = preparation.submit([] { return 6 * 7; }); +``` + +Real preparation should return owned CPU data. Retain/read its future and send +completed results to platform for upload; the group's cap cannot exceed physical +pool capacity and does not create two additional threads. + +## Limits and further reading + +No work stealing, forced termination, live pool reconfiguration or NUMA memory +placement is implied. Topology discovery does not prove a performance benefit. +Read [worker contracts and optional integration](../runtime/worker-execution.md), +[runtime shutdown](../runtime/runtime-frame-boundary.md#shutdown) and +[pool/group declarations](../../projects/engine/include/cheryl/core/engine/worker-pool.h). diff --git a/docs/generation/Doxyfile.in b/docs/generation/Doxyfile.in index a28586fb..39eaff26 100644 --- a/docs/generation/Doxyfile.in +++ b/docs/generation/Doxyfile.in @@ -48,7 +48,7 @@ PREDEFINED = CHERYL_NATIVE_INPUT=1 \ CE_NATIVE_GLFW_API= CE_OPENGL_API= \ CE_UI_TGUI_API= CE_UI_RMLUI_API= CE_AUDIO_MINIAUDIO_API= \ CE_STARTUP_API= CE_OPENGL_STARTUP_API= CE_DEBUG_TERMINAL_API= \ - CE_WORKER_TOPOLOGY_HWLOC_API= CE_WORKER_TOPOLOGY_JSON_API= + CE_WORKER_TOPOLOGY_HWLOC_API= CE_WORKER_TOPOLOGY_JSON_API= CE_WORKERS_API= MACRO_EXPANSION = YES EXPAND_ONLY_PREDEF = YES SEARCH_INCLUDES = NO diff --git a/docs/generation/build.py b/docs/generation/build.py index f8cf60f0..3d0e8aad 100644 --- a/docs/generation/build.py +++ b/docs/generation/build.py @@ -26,19 +26,19 @@ os.environ["PYTHONPYCACHEPREFIX"] = sys.pycache_prefix from bootstrap import atomic_json, file_lock, read_json, validate_doxygen +from source_revision import refresh_html_sources PUBLIC_INCLUDE_ROOTS = tuple( REPO_ROOT / name for name in ( "projects/engine/include", - "projects/engine/support/startup/include", - "projects/engine/support/worker-topology/include", - "projects/engine/support/debug-terminal/include", + "projects/modules/runtime/startup/include", + "projects/modules/runtime/workers/include", + "projects/modules/platform/debug-terminal-linux/private/startup/include", "projects/modules/platform/native-glfw/include", "projects/modules/platform/debug-terminal-linux/include", "projects/modules/graphics/opengl/include", - "projects/modules/graphics/opengl/support/startup/include", "projects/modules/ui/tgui/include", "projects/modules/ui/rmlui/include", "projects/modules/audio/miniaudio/include", @@ -110,6 +110,7 @@ def provenance(commit: str, doxygen_version: str, output: Path) -> dict: "source_repository": "https://github.com/cppcooper/cheryl-engine", "source_revision": commit, "source_ref": ref, + "source_dependencies": source_dependencies(commit), "dirty": dirty, "header_count": len(header_inputs()), "tools": { @@ -127,6 +128,19 @@ def provenance(commit: str, doxygen_version: str, output: Path) -> dict: } +def source_dependencies(commit: str) -> dict[str, str]: + entries = subprocess.check_output(["git", "ls-tree", "-rz", commit], cwd=REPO_ROOT) + dependencies = {} + for entry in entries.split(b"\0"): + if not entry: + continue + metadata, filename = entry.split(b"\t", 1) + mode, _, identity = metadata.split(b" ", 2) + if mode == b"160000" or filename == b".gitmodules": + dependencies[filename.decode("utf-8")] = identity.decode("ascii") + return dependencies + + def doxygen_configuration(output: Path, commit: str) -> str: headers = header_inputs() template = Path(__file__).with_name("Doxyfile.in").read_text(encoding="utf-8") @@ -163,10 +177,11 @@ def timed_stage(name: str): print(f"{name} took {time.monotonic() - started:.1f} seconds.", flush=True) -def extraction_fingerprint(output: Path, doxygen: str, commit: str) -> str: +def extraction_fingerprint(output: Path, doxygen: str) -> str: return fingerprint({ "format": CACHE_FORMAT, - "configuration": doxygen_configuration(output, commit), + # PROJECT_NUMBER records extraction provenance; it does not change the API. + "configuration": doxygen_configuration(output, ""), "headers": { path.relative_to(REPO_ROOT).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() for path in header_inputs() @@ -186,7 +201,8 @@ def rendering_inputs(output: Path, metadata: dict, xml_files: dict[str, str]) -> "format": CACHE_FORMAT, "repository": str(REPO_ROOT), "output": str(output), - "revision": metadata["source_revision"], + # Cached source links into submodules depend on their pinned revisions. + "source_dependencies": metadata.get("source_dependencies", {}), "tools": metadata["tools"], "python": str(Path(sys.executable).resolve()), "requirements": metadata["requirements_sha256"], @@ -344,7 +360,10 @@ def main() -> int: ) parser.add_argument("--cache-dir", type=Path, help="Shared documentation tool cache directory.") parser.add_argument("--doxygen", help="Use this Doxygen executable explicitly.") + parser.add_argument("--jobs", type=int, default=1, help="Positive number of Sphinx workers (default: 1).") arguments = parser.parse_args() + if arguments.jobs < 1: + parser.error("--jobs must be positive.") force_pinned = arguments.force_pinned or os.environ.get("CHERYL_DOCS_FORCE_PINNED") == "1" if arguments.doxygen is not None and force_pinned: parser.error("An explicit Doxygen override cannot be combined with forced pinned tools.") @@ -405,7 +424,7 @@ def main() -> int: state = read_json(state_path) if not isinstance(state, dict) or state.get("format") != CACHE_FORMAT: state = {} - extract_key = extraction_fingerprint(output, doxygen, commit) + extract_key = extraction_fingerprint(output, doxygen) xml_files = file_hashes(output / "xml") reuse_xml = ( not arguments.clean @@ -458,7 +477,7 @@ def main() -> int: changed = [] if isinstance(previous, dict): changed = [name for name in render_inputs if previous.get(name) != render_inputs[name]] - labels = ", ".join(changed or ["revision/config/tools/XML inventory"]) + labels = ", ".join(changed or ["configuration/tools/XML inventory"]) reasons.append(f"rendering inputs changed ({labels})") if not (output / "doctrees/environment.pickle").is_file(): reasons.append("cached Sphinx environment is missing") @@ -492,6 +511,8 @@ def main() -> int: str(GENERATION_ROOT), "-b", "html", + "-j", + str(arguments.jobs), "-W", "--keep-going", "-n", @@ -507,6 +528,12 @@ def main() -> int: env=environment, check=True, ) + # Sphinx writes changed pages using the current revision. Refresh links + # and the revision footer in retained HTML without resolving every API + # page and rendering the site's navigation again. + with timed_stage("Source revision refresh"): + refreshed = refresh_html_sources(output / "html", commit) + print(f"Updated source revision links in {refreshed} cached HTML pages.", flush=True) (output / "html/revision.json").write_text( json.dumps(metadata, indent=2, sort_keys=True) + "\n", encoding="utf-8" ) diff --git a/docs/generation/conf.py b/docs/generation/conf.py index c484e5e3..ce4d9c7e 100644 --- a/docs/generation/conf.py +++ b/docs/generation/conf.py @@ -15,6 +15,7 @@ if sys.pycache_prefix is None: sys.pycache_prefix = str(GENERATED_ROOT / "pycache") os.environ["PYTHONPYCACHEPREFIX"] = sys.pycache_prefix +sys.path.insert(0, str(GENERATION_ROOT)) sys.path.insert(0, str(GENERATION_ROOT / "extensions")) project = "Cheryl Engine" @@ -23,7 +24,8 @@ source_revision = os.environ.get("CHERYL_DOCS_REVISION") or subprocess.check_output( ["git", "rev-parse", "HEAD"], cwd=REPO_ROOT, text=True ).strip() -release = os.environ.get("READTHEDOCS_VERSION_NAME", source_revision[:12]) +# Revision identity is refreshed separately from Sphinx's parsed-page cache. +release = os.environ.get("READTHEDOCS_VERSION_NAME", "") version = release source_repository = "https://github.com/cppcooper/cheryl-engine" source_repository_root = str(REPO_ROOT) @@ -107,8 +109,6 @@ "display_github": True, "github_user": "cppcooper", "github_repo": "cheryl-engine", - "github_version": source_revision, "conf_py_path": "/docs/", - "source_revision": source_revision, "source_dirty": os.environ.get("CHERYL_DOCS_DIRTY") == "1", } diff --git a/docs/generation/extensions/repository_links.py b/docs/generation/extensions/repository_links.py index ea98589c..4e423291 100644 --- a/docs/generation/extensions/repository_links.py +++ b/docs/generation/extensions/repository_links.py @@ -17,6 +17,19 @@ from pygments.lexers.special import TextLexer from api_rendering import namespace_identity from cpp_references import CppReferences +from source_revision import source_url_at_revision + + +PROJECTED_GUIDES = { + "projects/modules/platform/native-glfw/README.md": "modules/native-glfw", + "projects/modules/graphics/opengl/README.md": "modules/opengl", + "projects/modules/ui/tgui/README.md": "modules/tgui", + "projects/modules/ui/rmlui/README.md": "modules/rmlui", + "projects/modules/audio/miniaudio/README.md": "modules/miniaudio", + "projects/modules/platform/debug-terminal-linux/README.md": "modules/debug-terminal-linux", + "projects/modules/runtime/startup/README.md": "modules/startup", + "projects/modules/runtime/workers/README.md": "modules/workers", +} @dataclass(frozen=True) @@ -114,6 +127,18 @@ def reference(node: nodes.Element, url: str) -> nodes.reference: return replacement +def projected_reference(app, node: nodes.Element, path: Path, fragment: str): + root = app._cheryl_repository_links.root + path = path.resolve() + if not path.is_relative_to(root): + return None + document = PROJECTED_GUIDES.get(path.relative_to(root).as_posix()) + if document not in app.env.found_docs: + return None + content = nodes.inline("", "", *[child.deepcopy() for child in node.children]) + return make_refnode(app.builder, node.get("refdoc", app.env.docname), document, fragment, content) + + def rewrite_links(app, doctree: nodes.document) -> None: repository = app._cheryl_repository_links environment = app.env @@ -131,6 +156,10 @@ def rewrite_links(app, doctree: nodes.document) -> None: docname = environment.path2doc(str(path)) if docname in environment.found_docs: continue + projected = projected_reference(app, node, path, parsed.fragment) + if projected is not None: + node.replace_self(projected) + continue url = repository.source_url(path, parsed.fragment, parsed.query) if url: node.replace_self(reference(node, url)) @@ -143,6 +172,10 @@ def rewrite_links(app, doctree: nodes.document) -> None: continue for suffix in app.config.source_suffix: path = sources / (target + suffix) + projected = projected_reference(app, node, path, node.get("reftargetid") or "") + if projected is not None: + node.replace_self(projected) + break url = repository.source_url(path, node.get("reftargetid") or "") if url: node.replace_self(reference(node, url)) @@ -160,6 +193,10 @@ def rewrite_links(app, doctree: nodes.document) -> None: docname = environment.path2doc(str(path)) if docname in environment.found_docs: continue + projected = projected_reference(app, node, path, parsed.fragment) + if projected is not None: + node.replace_self(projected) + continue url = repository.source_url(path, parsed.fragment, parsed.query) if url: node.replace_self(reference(node, url)) @@ -177,6 +214,10 @@ def rewrite_links(app, doctree: nodes.document) -> None: docname = environment.path2doc(str(path)) if docname in environment.found_docs: continue + projected = projected_reference(app, node, path, parsed.fragment) + if projected is not None: + node.replace_self(projected) + continue url = repository.source_url(path, parsed.fragment, parsed.query) if url: node["refuri"] = url @@ -300,7 +341,19 @@ def cpp_reference(app, environment, node, content): ) +def refresh_links(app, doctree, document) -> None: + repository = app._cheryl_repository_links + for node in doctree.findall(nodes.reference): + url = node.get("refuri") + if url: + node["refuri"] = source_url_at_revision(url, repository.revision, repository.url) + + def page_context(app, pagename, templatename, context, doctree) -> None: + context["source_revision"] = app.config.source_revision + context["github_version"] = app.config.source_revision + context["release"] = app.config.release or app.config.source_revision[:12] + context["version"] = context["release"] context["declaring_source_url"] = None if pagename.startswith("generation/generated/api/"): for identifier, document in app._cheryl_doxygen_documents.items(): @@ -324,11 +377,12 @@ def page_context(app, pagename, templatename, context, doctree) -> None: def setup(app) -> dict[str, object]: app.add_config_value("source_repository", "", "env") app.add_config_value("source_repository_root", "", "env") - app.add_config_value("source_revision", "", "env") + app.add_config_value("source_revision", "", "") app.connect("builder-inited", prepare) app.connect("missing-reference", cpp_reference) # Run before download collectors: source files remain links, not copied assets. app.connect("doctree-read", rewrite_links, priority=100) + app.connect("doctree-resolved", refresh_links, priority=100) app.connect("html-page-context", page_context) # Existing Mermaid fences remain readable source until a diagram renderer is # selected. Treat them as text explicitly instead of suppressing warnings. diff --git a/docs/generation/source_revision.py b/docs/generation/source_revision.py new file mode 100644 index 00000000..0d6f92ab --- /dev/null +++ b/docs/generation/source_revision.py @@ -0,0 +1,52 @@ +"""Refresh source identity without rendering unchanged documentation again.""" + +from __future__ import annotations + +from pathlib import Path +import re + + +SOURCE_REPOSITORY = "https://github.com/cppcooper/cheryl-engine" +SOURCE_PATH = re.compile(r"(?:blob|tree)/(?P[0-9a-fA-F]{64}|[0-9a-fA-F]{40})(?=[/?#]|$)") +SOURCE_ANCHOR = re.compile( + r"(?P]*?\shref=)(?P[\"'])" + r"(?Phttps://github\.com/[^\"']+)(?P=quote)" + r"(?P[^>]*>)(?P

Player name
+ + + )"); + if (!document_) + throw std::runtime_error("Cannot load the UI document"); + document_->Show(Rml::ModalFlag::None, Rml::FocusFlag::None); + auto* field = document_->GetElementById("name"); + if (!field || !field->Focus()) + throw std::runtime_error("Cannot focus the UI field"); + ui_->request_keyboard_focus(); +} +``` + +The document owns its elements; `document_` borrows the session's context. Font +bytes remain valid through Core shutdown. Routing/capture affect later polls, so +text already captured under another target does not move to the new field. + +### Process input and publish a scene + +After creation, each `update()` supplies copied sizes, simulation time and the +complete tick batch once: + +```cpp +ui_->set_view(tick.logical_size, tick.framebuffer_size); +ui_->update_time(tick.delta_seconds); +ui_->handle_input(tick.input.records(), true); +static_cast(CE::UI::RmlUi::adopt_scene(pending_, scene_)); +if (!pending_.valid()) + pending_ = uploader_->submit(engine_.platform_dispatcher().submission(), ui_->record(), materials_); +``` + +`true` explicitly assigns pointer input to this interface. Adopt only a ready, +complete upload; the first scene can be empty while the request is pending. +An upload/cancellation error propagates without replacing the previous scene. +The demo catches that error to report it and continue; propagation here selects +normal runtime failure cleanup. Keeping one pending replacement bounds the queue. + +During `prepare_render_frame()`, after world/HUD passes: + +```cpp +scene_.write(frame); +``` + +The frame contains retained packets, never live document operations. To end +editing on the UI owner, call `ui_->release_keyboard_focus()`; native window close +remains available while the field owns keyboard gameplay input. Application focus +shortcuts use the session's ordered `before_record` overload, as in the demo. + +### Close after UI quiescence + +During `deinit()`, after simulation joins and accepted dependencies settle: + +```cpp +document_ = nullptr; +if (ui_) { + ui_->close_after_quiescence(); + ui_.reset(); +} +scene_ = CE::UI::RmlUi::Scene{}; +uploader_.reset(); +materials_ = CE::UI::RmlUi::Materials{}; +``` + +Detach and release any listeners or toolkit resources you add before closing the +session. Keep the rest of game cleanup tolerant of partial initialization. +[Ownership and Cleanup](../../../../docs/architecture/ownership-cleanup.md) +connects these steps to the runtime's shutdown dependencies. + +The field should appear below the label, accept committed text and editing keys, +retain its placement during resize, and close cleanly in both runtime modes. This +minimal interface uses supported rectangular drawing. Clipboard, cursor services, +IME, transforms and advanced effects retain the capability limits below. + +## Native documents and input + +Create `Session` on the simulation/UI owner and call `context()` for native RML +document loading, RCSS styling, elements and listeners. The adapter contains no +layout string parser or common widget API. `load_font` registers a named family +from an owned file buffer or copied memory bytes. Both remain valid through Core +shutdown. The session owns one global Core initialization; a second session or +host-installed interface rejects instead of replacing the host's SDK state. + +Supply copied logical/framebuffer dimensions with `set_view`. Context dimensions +are logical units and its density-independent ratio is one; framebuffer scaling +belongs to Cheryl's clip/projection playback. `update_time` advances the supplied +simulation duration and updates native layout/animation. `record` updates pending +authoring/input changes before rendering without advancing the clock again. + +The session holds Events capture. `request_keyboard_focus` acquires Text capture +and a keyboard routing lease for its target. +`handle_input` observes modifier snapshots but delivers keyboard/text only for its +target and requested epoch saved at call entry. Pass the complete immutable tick +record batch once: poll-latched records drain before external focus preemption +clears native editing, including when the batch is empty. + +The three-argument overload calls `before_record(record)` before each delivery +and uses its return value as that record's pointer selection. Application controls +can request focus or show/hide a view in that callback without changing the entry +epoch used to select keyboard/text for the rest of the batch. Future polls observe +the new routing lease; existing records retain their original targets/epochs. +The two-argument overload keeps a constant pointer selection. Unselected pointer +delivery sends mouse-leave when necessary. Mouse buttons and fractional wheel +input apply their observation-time coordinates before delivery. Physical keys, +repeats and releases stay independent of committed Unicode text. + +Capabilities report keyboard/text support and rectangular clipping. Clipboard, +cursor changes, IME and advanced effects are unavailable. Clipboard shortcuts are +suppressed before they change a selection; explicit clipboard interface calls +reject. The adapter never runs a native toolkit backend or window event loop. + +Release external element, event-listener and toolkit-resource references before +session destruction. After the runtime joins simulation, `close_after_quiescence` +permits final platform-owner teardown. Native contexts and font globals shut down +while interface/font storage is still live. CPU recordings and uploaded Cheryl +frames retain their own geometry/pixels/resources independently. + +## Recording and publication + +`RenderTarget` implements the Core render interface without graphics calls. It +copies indexed triangles during compilation and translated draws during recording. +Texture handles retain immutable decoded generations; releasing a toolkit handle +does not invalidate a completed recording. Generated premultiplied pixels remain +unchanged; decoded file RGB is multiplied by alpha once. Texture V coordinates +map RmlUi's downward-positive convention to Cheryl's upward-positive image UVs. + +Rectangular scissor edges and logical dimensions travel with every draw. Zero +dimensions or empty clips produce no draws. Masks, nonidentity transforms, layers, +filters, custom shaders and repeating texture coordinates reject publication. +The failure stays latched for that renderer because the toolkit may cache an +unsupported compiled property without offering it again. Recreate the session +after removing such a property. SDK callbacks allow render-stack cleanup to finish +before the recording reports the error. + +Session texture bounds are at least 1024, matching the retained font atlas's +maximum dimension; the default bound is 4096. File/generated images must fit the +configured bound. Standalone CPU `RenderTarget` checks can use smaller bounds. + +The application supplies `Materials` with colored triangle pipelines, +`PremultipliedAlpha`, disabled depth/write/culling, a projection semantic and a +custom image sampler for textured draws. Shaders multiply image RGBA by vertex +RGBA without an additional premultiplication. `SceneUploader` belongs to one +provider/domain and platform owner; `submit` transfers owned recording data through +Cheryl's dispatcher. `adopt_scene` never waits and replaces the retained scene only +after a complete successful upload. Failure or cancellation preserves its predecessor. + +## Input identities + +`keyboard_key` maps portable physical keyboard identities to RmlUi keys; unknown +keys remain `KI_UNKNOWN`. Committed characters come from `TextEvent`, not from +this mapping. `modifiers` preserves Control, Shift, Alt, Super, Caps Lock and Num +Lock flags in RmlUi's modifier representation. + +## Material construction + +The demo retains manual pipeline/material construction on the graphics owner; +the adapter receives compatible premultiplied materials from its caller. The +[manual example](../../../../docs/rendering/pipelines-and-materials.md#manual-material-construction) +shows typed definitions and explicit uniform wiring. Indexed loading migrates +only main text/image materials. A later UI migration targets TGUI first; migrating +RmlUi also requires preserving a separately usable manual example. Core support +and planned effects remain independently scoped. + +## Dependency configuration + +It links shared RmlUi Core and `Cheryl::Engine`. Session installs a font engine +whose reviewed upstream provider, atlas, effects and fallback logic live in Core; +the Cheryl adapter supplies Engine rasterization through private callbacks. +Core itself does not link Engine, preserving that boundary with static Cheryl too. +The module provides native document sessions, portable input translation, CPU +recording and retained scene upload. The +[adapter-author guide](../../../../docs/development/ui-adapters.md) describes the +engine boundaries; RmlUi keeps its native context, RML and RCSS authoring API +exported from the shared toolkit library. + +This core integration is sufficient for the selected Linux root-composition scope. +Further rendering effects are +[planned extensions](../../../../docs/planning/long-term/README.md#ui-adapters), +including any required graphics-backend support. OS services remain deferred. +Dedicated regression and supplied-Core acceptance gaps remain in the +[startup/UI follow-up](../../../../docs/planning/develop-review-and-development-plan.md#startup-and-ui-follow-up). + The module reuses an existing `RmlUi::Core`. Otherwise it uses an explicit `CHERYL_RMLUI_SOURCE`, an explicit package location in `RmlUi_DIR`, the pinned `extern/rmlui` submodule, or the exact package, in that order. Initialize the bundled dependency with `git submodule update --init extern/rmlui`; CMake does not download -it. Supplied targets must declare `RMLUI_FONT_ENGINE=freetype`; ordinary source and -package configurations provide that metadata. - -An owned source configuration selects FreeType/Core and excludes samples, toolkit +it. Core is configured with `RMLUI_FONT_ENGINE=none`; the module installs its +Engine-backed implementation explicitly. Supplied shared targets declare +`CHERYL_RMLUI_ENGINE_SERVICES=TRUE` and `CHERYL_RMLUI_RASTERIZER_ABI=1` after +implementing the reviewed private rasterizer bridge and preserving native API exports. Metadata does +not make a stock incompatible binary satisfy those contracts. + +The adapter requires the matching reviewed font integration sources even when +Core comes from a package; supply `CHERYL_RMLUI_SOURCE` or retain the pinned +`extern/rmlui` checkout. The private callback signatures use those upstream font +headers. Session initialization binds the callback table only after acquiring +toolkit ownership, and shutdown releases font faces before clearing it. Keep Core, +the adapter and Engine loaded while their objects or callbacks remain in use. + +An owned source configuration selects Core without a separate FreeType engine and excludes samples, toolkit tests, native backends, Lua and optional plugins. Conflicting settings reject instead of rewriting a host's choices. The module links Core directly, leaving the upstream debugger outside the adapter dependency graph. -For a standalone module or its `tests/consumer/` entry point, set -`CHERYL_REPOSITORY_ROOT` to the Cheryl checkout. The module reuses a supplied +The standalone module and its `tests/consumer/` entry point locate their enclosing +checkout; set `CHERYL_REPOSITORY_ROOT` to select another absolute checkout path. +The module reuses a supplied `Cheryl::Engine` or bootstraps Engine from that root, suppressing other module/demo/test selection in its local scope. Consumer/header checks are selected through `CHERYL_BUILD_CONSUMER_TESTS` at the root or by configuring the consumer @@ -51,7 +325,7 @@ The selected RmlUi 6.3 Core must include the empty-value correction in the empty editable value using the displayed placeholder's line length. When Cheryl creates Core from `CHERYL_RMLUI_SOURCE` or the bundled source, its -[helper](cmake/RmlUi.cmake) compiles a generated copy of `WidgetTextInput.cpp` with +[helper](./cmake/RmlUi.cmake) compiles a generated copy of `WidgetTextInput.cpp` with an early `GetValue().empty()` return. It excludes the original translation unit, preserves its include directory and leaves the dependency checkout unchanged. An already guarded source is reused; an unrecognized entry/source layout rejects @@ -77,8 +351,9 @@ endif() set_property(TARGET "${rmlui_core_target}" PROPERTY CHERYL_RMLUI_PLACEHOLDER_FIX TRUE) ``` -The Cheryl SDK preserves this property on its exported Core dependency, so SDK -consumers need no manual declaration. +Shared Cheryl SDK consumers use the integrated owner binary and require no +separate exported Core dependency or manual declaration. Supplied source +compositions still verify the Core correction and Engine-service configuration. The [placeholder investigation](../../../../docs/external-work/rmlui-placeholder-issue.md) preserves the source diagnosis, unmodified-upstream reproduction requirements and @@ -87,23 +362,6 @@ hit-testing observations in the Linux/X11/OpenGL demo; existing session tests do not directly exercise that correction. Supplied targets/packages still require independent host acceptance and correction declarations. -## Input identities - -`keyboard_key` maps portable physical keyboard identities to RmlUi keys; unknown -keys remain `KI_UNKNOWN`. Committed characters come from `TextEvent`, not from -this mapping. `modifiers` preserves Control, Shift, Alt, Super, Caps Lock and Num -Lock flags in RmlUi's modifier representation. - -## Material construction - -The demo retains manual pipeline/material construction on the graphics owner; -the adapter receives compatible premultiplied materials from its caller. The -[manual example](../../../../docs/rendering/pipelines-and-materials.md#manual-material-construction) -shows typed definitions and explicit uniform wiring. Indexed loading migrates -only main text/image materials. A later UI migration targets TGUI first; migrating -RmlUi also requires preserving a separately usable manual example. Core support -and planned effects remain independently scoped. - ## Focused checks | Target | Output / kind | Selection / coverage | @@ -131,78 +389,6 @@ Checks use the selected SDK's `Samples/assets/LatoLatin-Regular.ttf` fixture or an explicit `CHERYL_RMLUI_TEST_FONT` file. Missing fonts reject configuration; these cases do not silently skip font coverage or search the host's system fonts. -## Recording and publication - -`RenderTarget` implements the Core render interface without graphics calls. It -copies indexed triangles during compilation and translated draws during recording. -Texture handles retain immutable decoded generations; releasing a toolkit handle -does not invalidate a completed recording. Generated premultiplied pixels remain -unchanged; decoded file RGB is multiplied by alpha once. Texture V coordinates -map RmlUi's downward-positive convention to Cheryl's upward-positive image UVs. - -Rectangular scissor edges and logical dimensions travel with every draw. Zero -dimensions or empty clips produce no draws. Masks, nonidentity transforms, layers, -filters, custom shaders and repeating texture coordinates reject publication. -The failure stays latched for that renderer because the toolkit may cache an -unsupported compiled property without offering it again. Recreate the session -after removing such a property. SDK callbacks allow render-stack cleanup to finish -before the recording reports the error. - -Session texture bounds are at least 1024, matching the stock FreeType atlas's -maximum dimension; the default bound is 4096. File/generated images must fit the -configured bound. Standalone CPU `RenderTarget` checks can use smaller bounds. - -The application supplies `Materials` with colored triangle pipelines, -`PremultipliedAlpha`, disabled depth/write/culling, a projection semantic and a -custom image sampler for textured draws. Shaders multiply image RGBA by vertex -RGBA without an additional premultiplication. `SceneUploader` belongs to one -provider/domain and platform owner; `submit` transfers owned recording data through -Cheryl's dispatcher. `adopt_scene` never waits and replaces the retained scene only -after a complete successful upload. Failure or cancellation preserves its predecessor. - -## Native documents and input - -Create `Session` on the simulation/UI owner and call `context()` for native RML -document loading, RCSS styling, elements and listeners. The adapter contains no -layout string parser or common widget API. `load_font` registers a named family -from an owned file buffer or copied memory bytes. Both remain valid through Core -shutdown. The session owns one global Core initialization; a second session or -host-installed interface rejects instead of replacing the host's SDK state. - -Supply copied logical/framebuffer dimensions with `set_view`. Context dimensions -are logical units and its density-independent ratio is one; framebuffer scaling -belongs to Cheryl's clip/projection playback. `update_time` advances the supplied -simulation duration and updates native layout/animation. `record` updates pending -authoring/input changes before rendering without advancing the clock again. - -The session holds Events capture. `request_keyboard_focus` acquires Text capture -and a keyboard routing lease for its target. -`handle_input` observes modifier snapshots but delivers keyboard/text only for its -target and requested epoch saved at call entry. Pass the complete immutable tick -record batch once: poll-latched records drain before external focus preemption -clears native editing, including when the batch is empty. - -The three-argument overload calls `before_record(record)` before each delivery -and uses its return value as that record's pointer selection. Application controls -can request focus or show/hide a view in that callback without changing the entry -epoch used to select keyboard/text for the rest of the batch. Future polls observe -the new routing lease; existing records retain their original targets/epochs. -The two-argument overload keeps a constant pointer selection. Unselected pointer -delivery sends mouse-leave when necessary. Mouse buttons and fractional wheel -input apply their observation-time coordinates before delivery. Physical keys, -repeats and releases stay independent of committed Unicode text. - -Capabilities report keyboard/text support and rectangular clipping. Clipboard, -cursor changes, IME and advanced effects are unavailable. Clipboard shortcuts are -suppressed before they change a selection; explicit clipboard interface calls -reject. The adapter never runs a native toolkit backend or window event loop. - -Release external element, event-listener and toolkit-resource references before -session destruction. After the runtime joins simulation, `close_after_quiescence` -permits final platform-owner teardown. Native contexts and font globals shut down -while interface/font storage is still live. CPU recordings and uploaded Cheryl -frames retain their own geometry/pixels/resources independently. - ## Acceptance procedure For changes affecting RmlUi, reuse the existing native/OpenGL profile with tests @@ -306,7 +492,7 @@ do not establish compositor behavior, physical input or GPU resource retirement. The [demo guide](../../../apps/demo/README.md#interaction-checks) retains selected platform limits. -For a toolkit upgrade, recheck Core/FreeType feature metadata, premultiplied file +For a toolkit upgrade, recheck Core/Engine font-service metadata, premultiplied file and generated images, font-byte lifetime, placeholder correction/declarations and retained frames through teardown. Accept changed behavior independently before extending native consumers or the diff --git a/projects/modules/ui/rmlui/cmake/RmlUi.cmake b/projects/modules/ui/rmlui/cmake/RmlUi.cmake index 7ad2e949..ced9d981 100644 --- a/projects/modules/ui/rmlui/cmake/RmlUi.cmake +++ b/projects/modules/ui/rmlui/cmake/RmlUi.cmake @@ -41,10 +41,13 @@ function(cheryl_add_rmlui_source) if(NOT EXISTS "${CHERYL_RMLUI_SOURCE}/CMakeLists.txt") message(FATAL_ERROR "CHERYL_RMLUI_SOURCE must point to a RmlUi 6.3 source checkout") endif() - if(DEFINED RMLUI_FONT_ENGINE AND NOT RMLUI_FONT_ENGINE STREQUAL "freetype") - message(FATAL_ERROR "Cheryl UI needs RMLUI_FONT_ENGINE=freetype; use a dedicated dependency configuration") + if(DEFINED RMLUI_FONT_ENGINE AND NOT RMLUI_FONT_ENGINE STREQUAL "none") + message(FATAL_ERROR "Cheryl UI needs RMLUI_FONT_ENGINE=none; Engine owns font rasterization") endif() - set(RMLUI_FONT_ENGINE freetype) + # Upstream creates this cache entry with a freetype default even when a + # normal variable selects none. Seed the cache without replacing user input. + set(RMLUI_FONT_ENGINE none CACHE STRING "Font engine to build RmlUi with") + set(RMLUI_FONT_ENGINE none) set(optional_features RMLUI_SAMPLES RMLUI_TESTS RMLUI_SHELL RMLUI_LUA_BINDINGS RMLUI_LOTTIE_PLUGIN RMLUI_SVG_PLUGIN RMLUI_HARFBUZZ_SAMPLE RMLUI_IME_SAMPLE_USE_NOTO_FONTS RMLUI_TRACY_PROFILING RMLUI_CUSTOM_CONFIGURATION) @@ -55,18 +58,31 @@ function(cheryl_add_rmlui_source) set(${feature} OFF) endforeach() set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) - if(CHERYL_BUILD_SHARED_LIBS) - set(BUILD_SHARED_LIBS ON) - elseif(NOT DEFINED BUILD_SHARED_LIBS) - set(BUILD_SHARED_LIBS OFF) - endif() + set(BUILD_SHARED_LIBS ON) add_subdirectory("${CHERYL_RMLUI_SOURCE}" "${CMAKE_CURRENT_BINARY_DIR}/rmlui" EXCLUDE_FROM_ALL) - cheryl_shared_dependency(RmlUi::Core OWNED) + set(font_source "${CHERYL_RMLUI_SOURCE}/Source/Core/FontEngineDefault") + foreach(source IN ITEMS FontEngineInterfaceDefault FontFace FontFaceHandleDefault FontFaceLayer FontFamily FontProvider) + target_sources(rmlui_core PRIVATE "${font_source}/${source}.cpp") + endforeach() + target_sources(rmlui_core PRIVATE "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../private/font-provider.cpp") + target_include_directories(rmlui_core PRIVATE "${font_source}") + target_compile_features(rmlui_core PRIVATE cxx_std_17) + set_target_properties(rmlui_core PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES + RUNTIME_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}" + LIBRARY_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}" + ARCHIVE_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") + set_property(TARGET rmlui_core PROPERTY CHERYL_RMLUI_ENGINE_SERVICES TRUE) + set_property(TARGET rmlui_core PROPERTY CHERYL_RMLUI_RASTERIZER_ABI 1) + set_property(TARGET rmlui_core APPEND PROPERTY EXPORT_PROPERTIES + CHERYL_RMLUI_ENGINE_SERVICES CHERYL_RMLUI_RASTERIZER_ABI CHERYL_RMLUI_PLACEHOLDER_FIX) + set_property(TARGET rmlui_core PROPERTY CHERYL_OWNED_UI_DEPENDENCY TRUE) get_directory_property(rmlui_version DIRECTORY "${CHERYL_RMLUI_SOURCE}" DEFINITION PROJECT_VERSION) if(NOT rmlui_version STREQUAL "6.3") message(FATAL_ERROR "Cheryl UI requires RmlUi 6.3; selected source reports ${rmlui_version}") endif() cheryl_fix_rmlui_placeholder() set_property(TARGET rmlui_core PROPERTY CHERYL_RMLUI_PLACEHOLDER_FIX TRUE) - set(RMLUI_FONT_ENGINE freetype PARENT_SCOPE) + set(RMLUI_FONT_ENGINE none PARENT_SCOPE) endfunction() diff --git a/projects/modules/ui/rmlui/private/font-provider.cpp b/projects/modules/ui/rmlui/private/font-provider.cpp new file mode 100644 index 00000000..7af9e4c9 --- /dev/null +++ b/projects/modules/ui/rmlui/private/font-provider.cpp @@ -0,0 +1,101 @@ +#include "rasterizer-bridge.h" + +#include + +#include + +namespace { + const CherylRmlUiPrivate::RasterizerCallbacks* active_rasterizer{}; + + class FontProvider final : public Rml::FontEngineInterfaceDefault { + const CherylRmlUiPrivate::RasterizerCallbacks callbacks_; + bool initialized_ = false; + + public: + explicit FontProvider(const CherylRmlUiPrivate::RasterizerCallbacks& callbacks) + : callbacks_(callbacks) {} + + ~FontProvider() override { + if (initialized_) + Shutdown(); + } + + void Initialize() override { + if (active_rasterizer) + throw std::logic_error("RmlUi font rasterizer is already active"); + active_rasterizer = &callbacks_; + try { + Rml::FontEngineInterfaceDefault::Initialize(); + initialized_ = true; + } catch (...) { + active_rasterizer = nullptr; + throw; + } + } + + void Shutdown() override { + if (!initialized_) + return; + Rml::FontEngineInterfaceDefault::Shutdown(); + initialized_ = false; + active_rasterizer = nullptr; + } + }; +} + +extern "C" Rml::FontEngineInterface* +cheryl_rmlui_create_font_engine_v1(const CherylRmlUiPrivate::RasterizerCallbacks* callbacks) noexcept(false) { + if (!callbacks || callbacks->abi != 1 || callbacks->size != sizeof(*callbacks) || !callbacks->variations || !callbacks->load || + !callbacks->release || !callbacks->style || !callbacks->initialize || !callbacks->append || !callbacks->kerning || + !callbacks->has_kerning) + throw std::invalid_argument("RmlUi requires a complete rasterizer callback ABI 1"); + // Construction does not acquire the process-global provider. RmlUi binds it + // during Initialize, after the Cheryl session has checked host ownership. + return new FontProvider(*callbacks); +} + +namespace Rml::FreeType { + bool Initialise() { + return active_rasterizer != nullptr; + } + + void Shutdown() {} + + bool GetFaceVariations(const Span data, Vector& output, const int face_index) { + return active_rasterizer->variations(data, output, face_index); + } + + FontFaceHandleFreetype LoadFace(const Span data, const String& source, const int face_index, const int named_instance) { + return active_rasterizer->load(data, source, face_index, named_instance); + } + + bool ReleaseFace(const FontFaceHandleFreetype face) { + return active_rasterizer->release(face); + } + + void GetFaceStyle(const FontFaceHandleFreetype face, String* family, Style::FontStyle* style, Style::FontWeight* weight) { + active_rasterizer->style(face, family, style, weight); + } + + bool InitialiseFaceHandle( + const FontFaceHandleFreetype face, + const int font_size, + FontGlyphMap& glyphs, + FontMetrics& metrics, + const bool load_default_glyphs + ) { + return active_rasterizer->initialize(face, font_size, glyphs, metrics, load_default_glyphs); + } + + bool AppendGlyph(const FontFaceHandleFreetype face, const int font_size, const Character character, FontGlyphMap& glyphs) { + return active_rasterizer->append(face, font_size, character, glyphs); + } + + int GetKerning(const FontFaceHandleFreetype face, const int font_size, const Character left, const Character right) { + return active_rasterizer->kerning(face, font_size, left, right); + } + + bool HasKerning(const FontFaceHandleFreetype face) { + return active_rasterizer->has_kerning(face); + } +} diff --git a/projects/modules/ui/rmlui/private/rasterizer-bridge.h b/projects/modules/ui/rmlui/private/rasterizer-bridge.h new file mode 100644 index 00000000..2c6ae2b2 --- /dev/null +++ b/projects/modules/ui/rmlui/private/rasterizer-bridge.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include +#include + +namespace CherylRmlUiPrivate { + // Core copies this table. Face handles stay opaque and are released by the + // adapter that allocated them; callbacks outlive the initialized provider. + struct RasterizerCallbacks { + std::uint32_t abi = 1; + std::size_t size = sizeof(RasterizerCallbacks); + decltype(&Rml::FreeType::GetFaceVariations) variations{}; + decltype(&Rml::FreeType::LoadFace) load{}; + decltype(&Rml::FreeType::ReleaseFace) release{}; + decltype(&Rml::FreeType::GetFaceStyle) style{}; + decltype(&Rml::FreeType::InitialiseFaceHandle) initialize{}; + decltype(&Rml::FreeType::AppendGlyph) append{}; + decltype(&Rml::FreeType::GetKerning) kerning{}; + decltype(&Rml::FreeType::HasKerning) has_kerning{}; + }; +} + +// Explicit throwing specification preserves C++ exceptions under MSVC /EHc. +extern "C" RMLUICORE_API Rml::FontEngineInterface* +cheryl_rmlui_create_font_engine_v1(const CherylRmlUiPrivate::RasterizerCallbacks* callbacks) noexcept(false); diff --git a/projects/modules/ui/rmlui/src/font-service.cpp b/projects/modules/ui/rmlui/src/font-service.cpp new file mode 100644 index 00000000..9cc0145e --- /dev/null +++ b/projects/modules/ui/rmlui/src/font-service.cpp @@ -0,0 +1,258 @@ +/* +MIT License + +Copyright (c) 2008-2014 CodePoint Ltd, Shift Technology Ltd, and contributors +Copyright (c) 2019-2026 The RmlUi Team, and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +// Cheryl adaptation of RmlUi font bitmap conversion and downscaling through Engine. + +#include + +#include "font.h" +#include "../private/rasterizer-bridge.h" +#include +#include + +#include +#include +#include +#include + +namespace CE::UI::RmlUi::Detail { + using namespace Rml; + namespace { + struct Face { + CE::Text::FontRasterizer rasterizer; + int size{}; + }; + + Face& face(const FontFaceHandleFreetype handle) { + return *reinterpret_cast(handle); + } + + CE::Text::FontRasterSize size(const int height) { + return {height > 0 ? static_cast(height) : 0, CE::Text::FontSizing::Characters}; + } + + void downscale( + byte* destination, + const int output_width, + const int output_height, + const byte* source, + const int width, + const int height, + const int channels + ) { + const float x_scale = static_cast(output_width) / width; + const float y_scale = static_cast(output_height) / height; + const float sum_scale = x_scale * y_scale; + float y_end = 0; + for (int y_out = 0; y_out < output_height; ++y_out) { + const auto y_start = y_end; + y_end = std::min((y_out + 1) / y_scale, height - .001f); + float x_end = 0; + for (int x_out = 0; x_out < output_width; ++x_out) { + const auto x_start = x_end; + x_end = std::min((x_out + 1) / x_scale, width - .001f); + float sum[4]{}; + for (int y = static_cast(y_start); y <= static_cast(y_end); ++y) { + auto y_part = 1.f; + if (y == static_cast(y_start)) + y_part -= y_start - y; + if (y == static_cast(y_end)) + y_part -= y + 1 - y_end; + for (int x = static_cast(x_start); x <= static_cast(x_end); ++x) { + auto x_part = 1.f; + if (x == static_cast(x_start)) + x_part -= x_start - x; + if (x == static_cast(x_end)) + x_part -= x + 1 - x_end; + for (int channel = 0; channel < channels; ++channel) + sum[channel] += source[(static_cast(y) * width + x) * channels + channel] * y_part * x_part; + } + } + for (int channel = 0; channel < channels; ++channel) + destination[(static_cast(y_out) * output_width + x_out) * channels + channel] = + static_cast(std::min(sum[channel] * sum_scale, 255.f)); + } + } + } + + bool append(Face& owner, const int font_size, const Character character, FontGlyphMap& glyphs) { + if (!owner.rasterizer.covers(static_cast(character))) + return false; + const auto raster = owner.rasterizer.rasterize(static_cast(character), size(font_size), {.color = true}); + if (!raster || raster->width > static_cast(std::numeric_limits::max()) || + raster->height > static_cast(std::numeric_limits::max())) + return false; + FontGlyph glyph; + glyph.bearing = {raster->left, raster->top}; + glyph.advance = static_cast(raster->advance); + glyph.bitmap_dimensions = {static_cast(raster->width), static_cast(raster->height)}; + if (raster->bitmap_scale < 1) { + glyph.bearing = Vector2i(Vector2f(glyph.bearing) * raster->bitmap_scale); + glyph.advance = static_cast(glyph.advance * raster->bitmap_scale); + glyph.bitmap_dimensions = Vector2i(Vector2f(glyph.bitmap_dimensions) * raster->bitmap_scale); + } + glyph.color_format = raster->format == CE::Text::FontBitmapFormat::PremultipliedRGBA ? ColorFormat::RGBA8 : ColorFormat::A8; + const auto channels = glyph.color_format == ColorFormat::RGBA8 ? 4 : 1; + const auto count = static_cast(glyph.bitmap_dimensions.x) * glyph.bitmap_dimensions.y * channels; + if (count) { + glyph.bitmap_owned_data.reset(new byte[count]); + glyph.bitmap_data = glyph.bitmap_owned_data.get(); + if (raster->bitmap_scale < 1) + downscale( + glyph.bitmap_owned_data.get(), glyph.bitmap_dimensions.x, glyph.bitmap_dimensions.y, raster->pixels.data(), + static_cast(raster->width), static_cast(raster->height), channels + ); + else + std::copy(raster->pixels.begin(), raster->pixels.end(), glyph.bitmap_owned_data.get()); + } + owner.size = font_size; + return glyphs.emplace(character, std::move(glyph)).second; + } + } + + static bool GetFaceVariations(const Span data, Vector& output, const int face_index) { + if (face_index < 0) + return false; + try { + for (const auto& variation : CE::Text::FontRasterizer::variations({data.data(), data.size()}, face_index)) + output.push_back( + {static_cast(variation.weight), variation.width, static_cast(variation.named_instance)} + ); + return true; + } catch (const std::bad_alloc&) { + throw; + } catch (const std::exception&) { + return false; + } + } + + static FontFaceHandleFreetype LoadFace(const Span data, const String& source, const int face_index, const int named_instance) { + if (face_index < 0 || named_instance < 0) + return 0; + try { + auto owner = std::make_unique(); + owner->rasterizer = CE::Text::FontRasterizer::load({data.data(), data.size()}, face_index, named_instance); + return reinterpret_cast(owner.release()); + } catch (const std::bad_alloc&) { + throw; + } catch (const std::exception& error) { + Log::Message(Log::LT_ERROR, "Engine could not load font from '%s': %s", source.c_str(), error.what()); + return 0; + } + } + + static bool ReleaseFace(const FontFaceHandleFreetype handle) { + delete reinterpret_cast(handle); + return true; + } + + static void GetFaceStyle(const FontFaceHandleFreetype handle, String* family, Style::FontStyle* style, Style::FontWeight* weight) { + const auto properties = face(handle).rasterizer.properties(); + if (family) + *family = properties.family; + if (style) + *style = properties.italic ? Style::FontStyle::Italic : Style::FontStyle::Normal; + if (weight) + *weight = static_cast(properties.weight); + } + + static bool InitialiseFaceHandle( + const FontFaceHandleFreetype handle, + const int font_size, + FontGlyphMap& glyphs, + FontMetrics& metrics, + const bool load_default_glyphs + ) { + auto& owner = face(handle); + const auto value = owner.rasterizer.metrics(size(font_size)); + if (!value) + return false; + owner.size = font_size; + metrics = { + font_size, + value->ascent, + -value->descent, + value->line_spacing, + value->x_height, + value->underline_position, + std::max(value->underline_thickness, 1.f), + value->has_ellipsis + }; + if (load_default_glyphs) { + glyphs.reserve(128); + for (unsigned int character = 32; character <= 126; ++character) + static_cast(append(owner, font_size, static_cast(character), glyphs)); + } + if (!glyphs.contains(Character::Replacement)) { + FontGlyph replacement; + replacement.bitmap_dimensions = {font_size / 3, font_size * 2 / 3}; + replacement.advance = replacement.bitmap_dimensions.x + 2; + replacement.bearing = {1, replacement.bitmap_dimensions.y}; + const auto count = static_cast(replacement.bitmap_dimensions.x) * replacement.bitmap_dimensions.y; + replacement.bitmap_owned_data.reset(new byte[count]); + replacement.bitmap_data = replacement.bitmap_owned_data.get(); + for (int y = 0; y < replacement.bitmap_dimensions.y; ++y) + for (int x = 0; x < replacement.bitmap_dimensions.x; ++x) + replacement.bitmap_owned_data[static_cast(y) * replacement.bitmap_dimensions.x + x] = + x == 0 || y == 0 || x + 1 == replacement.bitmap_dimensions.x || y + 1 == replacement.bitmap_dimensions.y ? 0xdd : 0; + glyphs.emplace(Character::Replacement, std::move(replacement)); + } + return true; + } + + static bool AppendGlyph(const FontFaceHandleFreetype handle, const int font_size, const Character character, FontGlyphMap& glyphs) { + return append(face(handle), font_size, character, glyphs); + } + + static int GetKerning(const FontFaceHandleFreetype handle, const int font_size, const Character left, const Character right) { + auto& owner = face(handle); + if (font_size > 0) + owner.size = font_size; + const auto raster_size = size(owner.size); + const auto metrics = owner.rasterizer.metrics(raster_size); + if (!metrics || metrics->bitmap_scale != 1) + return 0; + return static_cast(owner.rasterizer.kerning(static_cast(left), static_cast(right), raster_size)); + } + + static bool HasKerning(const FontFaceHandleFreetype handle) { + return face(handle).rasterizer.properties().kerning; + } + + const CherylRmlUiPrivate::RasterizerCallbacks& rasterizer_callbacks() { + static const CherylRmlUiPrivate::RasterizerCallbacks callbacks{ + 1, + sizeof(CherylRmlUiPrivate::RasterizerCallbacks), + GetFaceVariations, + LoadFace, + ReleaseFace, + GetFaceStyle, + InitialiseFaceHandle, + AppendGlyph, + GetKerning, + HasKerning + }; + return callbacks; + } +} diff --git a/projects/modules/ui/rmlui/src/font.cpp b/projects/modules/ui/rmlui/src/font.cpp new file mode 100644 index 00000000..436af15e --- /dev/null +++ b/projects/modules/ui/rmlui/src/font.cpp @@ -0,0 +1,9 @@ +#include "font.h" + +#include "../private/rasterizer-bridge.h" + +namespace CE::UI::RmlUi::Detail { + std::unique_ptr make_font_engine() { + return std::unique_ptr{cheryl_rmlui_create_font_engine_v1(&rasterizer_callbacks())}; + } +} diff --git a/projects/modules/ui/rmlui/src/font.h b/projects/modules/ui/rmlui/src/font.h new file mode 100644 index 00000000..5e8d5d0d --- /dev/null +++ b/projects/modules/ui/rmlui/src/font.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include + +namespace CherylRmlUiPrivate { + struct RasterizerCallbacks; +} + +namespace CE::UI::RmlUi::Detail { + [[nodiscard]] const CherylRmlUiPrivate::RasterizerCallbacks& rasterizer_callbacks(); + [[nodiscard]] std::unique_ptr make_font_engine(); +} diff --git a/projects/modules/ui/rmlui/src/session.cpp b/projects/modules/ui/rmlui/src/session.cpp index 53b2aa3d..1d28be9f 100644 --- a/projects/modules/ui/rmlui/src/session.cpp +++ b/projects/modules/ui/rmlui/src/session.cpp @@ -1,6 +1,8 @@ #include #include +#include "font.h" + #include #include @@ -46,6 +48,8 @@ namespace CE::UI::RmlUi { }; class LibraryLease final { + std::unique_ptr fonts_ = Detail::make_font_engine(); + public: LibraryLease(System& system, RenderTarget& renderer) { const std::lock_guard lock(library_mutex); @@ -54,17 +58,17 @@ namespace CE::UI::RmlUi { throw Exceptions::failed_operation(CE_HERE, "RmlUi already has an active session or host-installed interfaces"); Rml::SetSystemInterface(&system); Rml::SetRenderInterface(&renderer); + Rml::SetFontEngineInterface(fonts_.get()); try { if (!Rml::Initialise()) - throw Exceptions::failed_operation( - CE_HERE, "RmlUi initialization failed; verify the required stock FreeType configuration" - ); + throw Exceptions::failed_operation(CE_HERE, "RmlUi initialization failed with the Engine font service"); } catch (...) { // Core 6.3 cannot Shutdown a partially initialized SDK. Its // dependency contract prevents missing default interfaces; // never leave our failed constructor's storage installed. Rml::SetSystemInterface(nullptr); Rml::SetRenderInterface(nullptr); + Rml::SetFontEngineInterface(nullptr); throw; } library_owned = true; diff --git a/projects/modules/ui/rmlui/tests/unit/CMakeLists.txt b/projects/modules/ui/rmlui/tests/unit/CMakeLists.txt index 592dfea0..cfdaad64 100644 --- a/projects/modules/ui/rmlui/tests/unit/CMakeLists.txt +++ b/projects/modules/ui/rmlui/tests/unit/CMakeLists.txt @@ -6,6 +6,7 @@ cheryl_add_test_suite(${TARGET_TEST_MODULE_UI_RMLUI} OUTPUT_NAME "${OUTPUT_TEST_MODULE_UI_RMLUI}" SOURCES ${rmlui_test_sources} LIBRARIES ${LINKAGE_PUBLIC_TEST_MODULE_UI_RMLUI} + PRIVATE_INCLUDES "${cheryl_rmlui_font_source}" DEFINITIONS CHERYL_RMLUI_TEST_IMAGE="${CMAKE_CURRENT_LIST_DIR}/../fixtures/rgba.png" CHERYL_RMLUI_TEST_BLUE_IMAGE="${CMAKE_CURRENT_LIST_DIR}/../fixtures/blue.png" CHERYL_RMLUI_TEST_FONT="${test_font}") diff --git a/projects/modules/ui/rmlui/tests/unit/src/rasterizer.cpp b/projects/modules/ui/rmlui/tests/unit/src/rasterizer.cpp new file mode 100644 index 00000000..5ca9c033 --- /dev/null +++ b/projects/modules/ui/rmlui/tests/unit/src/rasterizer.cpp @@ -0,0 +1,20 @@ +#include + +#include "../../../private/rasterizer-bridge.h" + +#include + +#include + +TEST(ui_rmlui_font, callback_abi) { + EXPECT_THROW(static_cast(cheryl_rmlui_create_font_engine_v1(nullptr)), std::invalid_argument); + CherylRmlUiPrivate::RasterizerCallbacks callbacks; + callbacks.abi = 2; + EXPECT_THROW(static_cast(cheryl_rmlui_create_font_engine_v1(&callbacks)), std::invalid_argument); + callbacks.abi = 1; + callbacks.size = 0; + EXPECT_THROW(static_cast(cheryl_rmlui_create_font_engine_v1(&callbacks)), std::invalid_argument); + callbacks.size = sizeof(callbacks); + EXPECT_THROW(static_cast(cheryl_rmlui_create_font_engine_v1(&callbacks)), std::invalid_argument); + EXPECT_EQ(Rml::GetFontEngineInterface(), nullptr); +} diff --git a/projects/modules/ui/rmlui/tests/unit/src/session.cpp b/projects/modules/ui/rmlui/tests/unit/src/session.cpp index d0b5158b..b23545d1 100644 --- a/projects/modules/ui/rmlui/tests/unit/src/session.cpp +++ b/projects/modules/ui/rmlui/tests/unit/src/session.cpp @@ -1,10 +1,12 @@ #include +#include #include #include "../../support/document.h" #include #include #include +#include #include #include @@ -79,6 +81,32 @@ TEST(ui_rmlui_session, lifecycle) { EXPECT_NE(Rml::GetSystemInterface(), nullptr); } +TEST(ui_rmlui_session, engine_font) { + Input input; + Session session(input, 7); + std::ifstream file(CHERYL_RMLUI_TEST_FONT, std::ios::binary); + std::vector bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + ASSERT_FALSE(bytes.empty()); + const auto face = CE::Text::FontRasterizer::load(bytes); + const auto expected = face.metrics({24, CE::Text::FontSizing::Characters}); + const auto glyph = face.rasterize(U'A', {24, CE::Text::FontSizing::Characters}, {.color = true}); + ASSERT_TRUE(expected); + ASSERT_TRUE(glyph); + ASSERT_TRUE(session.load_font(bytes, "engine-font")); + std::fill(bytes.begin(), bytes.end(), 0); + auto* fonts = Rml::GetFontEngineInterface(); + ASSERT_NE(fonts, nullptr); + const auto handle = fonts->GetFontFaceHandle("engine-font", Rml::Style::FontStyle::Normal, Rml::Style::FontWeight::Normal, 24); + ASSERT_NE(handle, 0u); + const auto& metrics = fonts->GetFontMetrics(handle); + EXPECT_EQ(metrics.ascent, expected->ascent); + EXPECT_EQ(metrics.descent, -expected->descent); + EXPECT_EQ(metrics.line_spacing, expected->line_spacing); + EXPECT_EQ(metrics.x_height, expected->x_height); + const Rml::String language; + EXPECT_EQ(fonts->GetStringWidth(handle, "A", {language}), static_cast(glyph->advance)); +} + TEST(ui_rmlui_session, routing) { Input input; Session session(input, 7); diff --git a/projects/modules/ui/tgui/CMakeLists.txt b/projects/modules/ui/tgui/CMakeLists.txt index 2f04dfcf..c89cc57b 100644 --- a/projects/modules/ui/tgui/CMakeLists.txt +++ b/projects/modules/ui/tgui/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.28) -include("${CHERYL_REPOSITORY_ROOT}/cmake/CherylModules.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/../../../../cmake/CherylModules.cmake") if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) project(cheryl_ui_tgui_module VERSION ${VERSION_PROJECT_MODULE_UI_TGUI} LANGUAGES CXX) endif() @@ -19,11 +19,20 @@ if(NOT TARGET TGUI::TGUI) include(cmake/TGUI.cmake) cheryl_add_tgui_source() else() - find_package(TGUI 1.13.0 EXACT CONFIG REQUIRED) + find_package(TGUI 1.13.0 EXACT CONFIG REQUIRED GLOBAL) endif() endif() -cheryl_shared_dependency(TGUI::TGUI) +get_target_property(tgui_engine_services TGUI::TGUI CHERYL_TGUI_ENGINE_SERVICES) +if(NOT tgui_engine_services) + message(FATAL_ERROR + "A supplied TGUI target must use the custom Engine font/image services configuration and declare " + "CHERYL_TGUI_ENGINE_SERVICES=TRUE. Use CHERYL_TGUI_SOURCE for an owned compatible build.") +endif() +get_target_property(tgui_decoder_abi TGUI::TGUI CHERYL_TGUI_IMAGE_DECODER_ABI) +if(NOT tgui_decoder_abi STREQUAL "1") + message(FATAL_ERROR "A supplied TGUI shared runtime must provide CHERYL_TGUI_IMAGE_DECODER_ABI=1") +endif() file(GLOB_RECURSE cheryl_tgui_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") @@ -35,10 +44,32 @@ set_target_properties(${TARGET_LIB_MODULE_UI_TGUI} PROPERTIES OUTPUT_NAME "${OUTPUT_LIB_MODULE_UI_TGUI}" ARCHIVE_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") cheryl_configure_library(${TARGET_LIB_MODULE_UI_TGUI} UI_TGUI) +get_target_property(tgui_owned TGUI::TGUI CHERYL_OWNED_UI_DEPENDENCY) +if(tgui_owned) + cheryl_shared_dependency(TGUI::TGUI OWNER ${TARGET_LIB_MODULE_UI_TGUI} OWNED) +else() + cheryl_shared_dependency(TGUI::TGUI OWNER ${TARGET_LIB_MODULE_UI_TGUI}) +endif() target_include_directories(${TARGET_LIB_MODULE_UI_TGUI} PUBLIC "$" "$") -target_link_libraries(${TARGET_LIB_MODULE_UI_TGUI} PUBLIC ${LINKAGE_PUBLIC_LIB_MODULE_UI_TGUI}) +target_include_directories(${TARGET_LIB_MODULE_UI_TGUI} PUBLIC + "$>") +target_link_libraries(${TARGET_LIB_MODULE_UI_TGUI} PUBLIC Cheryl::Engine TGUI::TGUI) +# Native-only authors need the adapter's decoder registration even when no +# Session or other Cheryl symbol is referenced. Retain its C ABI anchor before +# static archive scanning and when linking with --as-needed or import libraries. +set(tgui_decoder_anchor cheryl_tgui_retain_image_loader) +if(WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 4) + string(PREPEND tgui_decoder_anchor "_") +endif() +if(MSVC) + target_link_options(${TARGET_LIB_MODULE_UI_TGUI} INTERFACE "LINKER:/INCLUDE:${tgui_decoder_anchor}") +elseif(APPLE) + target_link_options(${TARGET_LIB_MODULE_UI_TGUI} INTERFACE "LINKER:-u,_${tgui_decoder_anchor}") +else() + target_link_options(${TARGET_LIB_MODULE_UI_TGUI} INTERFACE "LINKER:-u,${tgui_decoder_anchor}") +endif() if(CHERYL_BUILD_TESTS) enable_testing() diff --git a/projects/modules/ui/tgui/README.md b/projects/modules/ui/tgui/README.md index 78aa6915..0a360dda 100644 --- a/projects/modules/ui/tgui/README.md +++ b/projects/modules/ui/tgui/README.md @@ -2,23 +2,27 @@ `Cheryl::UI::TGUI` is an optional UI owner. It translates selected Cheryl input records and records/uploads toolkit draws as retained Cheryl scenes. A `Session` -owns the custom backend, GUI, FreeType fonts and input leases. The +owns the custom backend, GUI, Engine-backed fonts and input leases. The [selected demo](../../../apps/demo/README.md) supplies a representative panel and application materials. -The module links `Cheryl::Engine` and TGUI 1.13.0, configured with a custom backend -and FreeType font support only. It has no Native GLFW/OpenGL/Gainput link. Public -headers expose TGUI's native GUI/widget API; Engine headers never include TGUI. +The module uses a shared TGUI 1.13.0 runtime configured with the custom backend. +Its Cheryl adapter supplies Engine font rasterization and image decoding, so TGUI +compiles neither another FreeType backend nor its bundled stb decoder. TGUI itself +does not link Engine, Native GLFW, OpenGL or Gainput. Public headers expose the +native GUI/widget API exported by the shared toolkit; Engine headers never include TGUI. The [adapter-author guide](../../../../docs/development/ui-adapters.md) describes the engine boundaries used by independently implemented UI modules. ## Contents - [Selection and dependency](#selection-and-dependency) +- [Use it in a game](#use-it-in-a-game) - [Session ownership](#session-ownership) - [Typed layout](#typed-layout) - [Input translation](#input-translation) - [Recording and publication](#recording-and-publication) +- [Dependency configuration](#dependency-configuration) - [Focused checks](#focused-checks) ## Selection and dependency @@ -26,33 +30,34 @@ the engine boundaries used by independently implemented UI modules. At the repository root, `CHERYL_BUILD_UI_TGUI=ON` is the default, independently of native/graphics selection. Set it to `OFF` to omit the toolkit; Engine retains its own text dependencies. Link the application to -`Cheryl::UI::TGUI`; its Engine and toolkit requirements propagate through the target. +`Cheryl::UI::TGUI`; its Engine and native-toolkit header/ABI settings propagate. The build target is `module_ui_tgui`, with output name `cheryl-module-ui-tgui`. ```cmake target_link_libraries(game PRIVATE Cheryl::UI::TGUI) ``` -The module reuses an existing `TGUI::TGUI` target. Otherwise it uses an explicit -`CHERYL_TGUI_SOURCE`, an explicit package location in `TGUI_DIR`, the pinned -`extern/tgui` submodule, or that exact version's discovered CMake package, in that -order. Initialize the bundled dependency with -`git submodule update --init extern/tgui`; CMake never downloads it. Source -composition requires FreeType development files and selects only the -custom/FreeType backend and excludes upstream tools, examples and tests. Conflicting -backend settings fail explicitly; supplied targets/settings are not rewritten. -Compiled version/feature checks also reject a supplied package/target with an -unreviewed version or native toolkit backend. +Initialize the pinned dependency through the checkout's setup instructions. +For supplied targets or packages, check [dependency configuration](#dependency-configuration) +before selecting the module. -Existing CMake/CLion profiles with `CHERYL_BUILD_UI_TGUI=OFF` cached must change it -to `ON` and reload CMake to expose `module_ui_tgui` and its selected test targets. +## Use it in a game -For a standalone module, set `CHERYL_REPOSITORY_ROOT` to the Cheryl checkout. -The module reuses a supplied `Cheryl::Engine` or bootstraps Engine from that root, -suppressing UI/native/graphics/demo/test selection in its local scope. -For an independently configured consumer, use `tests/consumer/` as the source -directory with the same dependency arguments; it links only this module and checks -its public header as the first include. +[Create an interactive GUI](../../../../docs/getting-started/how-to/create-gui.md) +provides the complete button/edit-field workflow, including materials and focus +controls. Its lifecycle is: + +1. During platform-owned `init()`, construct application materials and a scene uploader. +2. On the first simulation update, create a session and native widgets. +3. Supply copied sizes, simulation time and the complete input batch each update. +4. Record CPU scene data and keep one upload pending while retaining the previous scene. +5. Append the uploaded scene during frame preparation, after world/HUD passes. +6. After UI quiescence, release application-held toolkit objects and close the session. + +Use [typed layout](#typed-layout) to add anchoring and bounded resizing to that +working interface. Committed text and editing depend on input/focus support; +clipboard, cursor changes and IME remain unavailable services. The contracts below +cover session, layout and publication behavior. ## Session ownership @@ -60,8 +65,8 @@ Create `Session(input, nonzero_focus_id, options)` on the simulation/UI thread, before creating toolkit objects. A session installs TGUI's process-global backend; an already active toolkit backend/session rejects explicitly. Use `session.gui()` and TGUI's native widgets on that same owner. The session keeps Events capture -active, uses the toolkit's embedded default font and selects guarded FreeType -rasterization. `SessionOptions` supplies a texture bound of at least 128 and an +active and uses the toolkit's embedded default font through Engine rasterization. +`SessionOptions` supplies a texture bound of at least 128 and an explicit positive font scale; this is not a hardware or monitor-scale query. For each update, pass the tick's copied `logical_size` and `framebuffer_size` to @@ -93,7 +98,7 @@ selection. See the [adapter routing guide](../../../../docs/development/ui-adapt `capabilities()` reports the input source's committed-text/focus support and unavailable OS clipboard, cursor and IME services. Clipboard calls reject; standard toolkit copy/cut/paste shortcuts are skipped before widgets can delete a selection. -Cursor requests leave the platform cursor alone. Texture and FreeType font +Cursor requests leave the platform cursor alone. Texture and Engine-backed font smoothing can select either nearest or linear filtering; the selected value is copied into each recording rather than changing an uploaded shared image. @@ -182,12 +187,12 @@ Invalid Unicode scalars or nonfinite/unrepresentable wheel offsets reject explic `Renderer(maximum_size)` creates CPU-only `Texture` objects. The configured limit is an application-supported bound, not a hardware query. Each texture load copies top-to-bottom RGBA pixels into a fresh immutable snapshot, including the transient -FreeType atlas path. Old recordings retain their original snapshot. Base toolkit +font-atlas path. Old recordings retain their original snapshot. Base toolkit image loading also retains its pixels for transparent-pixel hit testing. `setSmooth` changes the toolkit's sampling selection without replacing its immutable pixel snapshot. Every recorded textured draw copies that selection, including -FreeType atlas draws. Upload requests a retained nearest or linear sampler through +font-atlas draws. Upload requests a retained nearest or linear sampler through `ResourceProvider::create_sampler`; both policies clamp to edge, disable mipmaps and disable anisotropy for toolkit atlas/pixel semantics. Engine image defaults remain anisotropic elsewhere. A provider without explicit sampler support rejects @@ -243,6 +248,41 @@ a UI pass at the caller's chosen point in pass order. Packets own uploaded image geometry, materials, parameters and clips, independent of subsequent widget changes or adapter destruction; native playback still requires the original live domain. +## Dependency configuration + +The module reuses an existing `TGUI::TGUI` target. Otherwise it uses an explicit +`CHERYL_TGUI_SOURCE`, an explicit package location in `TGUI_DIR`, the pinned +`extern/tgui` submodule, or that exact version's discovered CMake package, in that +order. Initialize the bundled dependency with +`git submodule update --init extern/tgui`; CMake never downloads it. Source +composition selects `TGUI_BACKEND=Custom` and +`TGUI_CUSTOM_BACKEND_HAS_FONT_FREETYPE=OFF`, excluding upstream tools, examples and tests. Conflicting +backend settings fail explicitly; supplied targets/settings are not rewritten. +Compiled version/feature checks also reject a supplied package/target with an +unreviewed version or native toolkit backend. Supplied shared targets declare +`CHERYL_TGUI_ENGINE_SERVICES=TRUE` and `CHERYL_TGUI_IMAGE_DECODER_ABI=1` after +implementing the reviewed private decoder bridge and preserving native API exports. Metadata +declares a verified configuration; it does not repair incompatible binaries. + +The toolkit decoder calls a versioned callback table installed by the Cheryl +adapter. Engine owns the decoded pixels until TGUI copies them into toolkit-owned +storage and invokes the supplied release callback. The adapter's CMake target +retains decoder registration even when an application uses only native TGUI APIs +and creates no Cheryl session. This boundary also keeps Engine out of the toolkit +DLL when Cheryl itself is static; keep both libraries loaded while their objects +and callbacks are in use. + +Existing CMake/CLion profiles with `CHERYL_BUILD_UI_TGUI=OFF` cached must change it +to `ON` and reload CMake to expose `module_ui_tgui` and its selected test targets. + +The standalone module locates its enclosing checkout; set +`CHERYL_REPOSITORY_ROOT` to select another absolute checkout path. +The module reuses a supplied `Cheryl::Engine` or bootstraps Engine from that root, +suppressing UI/native/graphics/demo/test selection in its local scope. +For an independently configured consumer, use `tests/consumer/` as the source +directory with the same dependency arguments; it links only this module and checks +its public header as the first include. + ## Focused checks | Target | Output / kind | Selection / coverage | @@ -322,4 +362,5 @@ and [global lifetime](https://github.com/texus/TGUI/blob/v1.13.0/src/Backend/Win when upgrading. Recheck unindexed glyph geometry, clip rounding, atlas replacement, clipboard suppression and teardown before native consumers depend on new behavior. The [upstream backend guide](https://tgui.eu/tutorials/latest-stable/backends/) -explains feature selection; supplied targets must retain the custom/FreeType scope. +explains feature selection; supplied targets must retain the reviewed Engine +font/image configuration, private decoder ABI and shared runtime policy. diff --git a/projects/modules/ui/tgui/cmake/TGUI.cmake b/projects/modules/ui/tgui/cmake/TGUI.cmake index f5fc1ea6..9325ef50 100644 --- a/projects/modules/ui/tgui/cmake/TGUI.cmake +++ b/projects/modules/ui/tgui/cmake/TGUI.cmake @@ -7,11 +7,11 @@ function(cheryl_add_tgui_source) if(DEFINED TGUI_BACKEND AND NOT TGUI_BACKEND STREQUAL "Custom") message(FATAL_ERROR "Cheryl UI needs TGUI_BACKEND=Custom; use a dedicated dependency configuration") endif() - if(DEFINED TGUI_CUSTOM_BACKEND_HAS_FONT_FREETYPE AND NOT TGUI_CUSTOM_BACKEND_HAS_FONT_FREETYPE) - message(FATAL_ERROR "Cheryl UI needs TGUI_CUSTOM_BACKEND_HAS_FONT_FREETYPE=ON") + if(DEFINED TGUI_CUSTOM_BACKEND_HAS_FONT_FREETYPE AND TGUI_CUSTOM_BACKEND_HAS_FONT_FREETYPE) + message(FATAL_ERROR "Cheryl UI needs TGUI_CUSTOM_BACKEND_HAS_FONT_FREETYPE=OFF; Engine owns font rasterization") endif() set(TGUI_BACKEND Custom) - set(TGUI_CUSTOM_BACKEND_HAS_FONT_FREETYPE ON) + set(TGUI_CUSTOM_BACKEND_HAS_FONT_FREETYPE OFF) set(native_features TGUI_HAS_BACKEND_SFML_GRAPHICS TGUI_HAS_BACKEND_SFML_OPENGL3 TGUI_HAS_BACKEND_SDL_GPU TGUI_HAS_BACKEND_SDL_RENDERER @@ -37,11 +37,25 @@ function(cheryl_add_tgui_source) set(TGUI_BUILD_TESTS OFF) set(TGUI_BUILD_DOC OFF) set(TGUI_INSTALL OFF) - if(CHERYL_BUILD_SHARED_LIBS) - set(TGUI_SHARED_LIBS ON) + if(DEFINED TGUI_SHARED_LIBS AND NOT TGUI_SHARED_LIBS) + message(FATAL_ERROR "Cheryl UI requires a shared TGUI runtime; remove TGUI_SHARED_LIBS=OFF") endif() + set(TGUI_SHARED_LIBS ON) add_subdirectory("${CHERYL_TGUI_SOURCE}" "${CMAKE_CURRENT_BINARY_DIR}/tgui" EXCLUDE_FROM_ALL) - cheryl_shared_dependency(TGUI::TGUI OWNED) + # Engine provides image decoding; do not compile TGUI's bundled stb decoder. + get_target_property(tgui_sources tgui SOURCES) + list(FILTER tgui_sources EXCLUDE REGEX "(^|/)Loading/ImageLoader\\.cpp$") + set_property(TARGET tgui PROPERTY SOURCES "${tgui_sources}") + target_sources(tgui PRIVATE "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../private/image-loader.cpp") + set_target_properties(tgui PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}" + LIBRARY_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}" + ARCHIVE_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") + set_property(TARGET tgui PROPERTY CHERYL_TGUI_ENGINE_SERVICES TRUE) + set_property(TARGET tgui PROPERTY CHERYL_TGUI_IMAGE_DECODER_ABI 1) + set_property(TARGET tgui APPEND PROPERTY EXPORT_PROPERTIES + CHERYL_TGUI_ENGINE_SERVICES CHERYL_TGUI_IMAGE_DECODER_ABI) + set_property(TARGET tgui PROPERTY CHERYL_OWNED_UI_DEPENDENCY TRUE) get_directory_property(tgui_version DIRECTORY "${CHERYL_TGUI_SOURCE}" DEFINITION TGUI_VERSION) if(NOT tgui_version STREQUAL "1.13.0") message(FATAL_ERROR "Cheryl UI requires TGUI 1.13.0; selected source reports ${tgui_version}") diff --git a/projects/modules/ui/tgui/private/image-decoder.h b/projects/modules/ui/tgui/private/image-decoder.h new file mode 100644 index 00000000..46034811 --- /dev/null +++ b/projects/modules/ui/tgui/private/image-decoder.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include +#include + +namespace CherylTguiPrivate { + enum class DecodeStatus : std::uint32_t { Success, Invalid, OutOfMemory }; + + // The decoder retains allocation ownership. ImageLoader copies the pixels + // into toolkit storage before invoking release, including on failure. + struct DecodedImage { + const std::uint8_t* pixels{}; + std::size_t bytes{}; + std::uint32_t width{}; + std::uint32_t height{}; + void* owner{}; + void (*release)(void*) noexcept {}; + }; + + using ImageDecoder = DecodeStatus (*)(const std::uint8_t*, std::size_t, DecodedImage&) noexcept; +} + +extern "C" { +TGUI_API CherylTguiPrivate::ImageDecoder cheryl_tgui_exchange_image_decoder_v1(CherylTguiPrivate::ImageDecoder decoder) noexcept; +TGUI_API bool +cheryl_tgui_restore_image_decoder_v1(CherylTguiPrivate::ImageDecoder expected, CherylTguiPrivate::ImageDecoder replacement) noexcept; +} diff --git a/projects/modules/ui/tgui/private/image-loader.cpp b/projects/modules/ui/tgui/private/image-loader.cpp new file mode 100644 index 00000000..094f03ea --- /dev/null +++ b/projects/modules/ui/tgui/private/image-loader.cpp @@ -0,0 +1,58 @@ +#include "image-decoder.h" + +#include +#include + +#include +#include +#include +#include + +namespace { + std::atomic image_decoder{nullptr}; + + struct ImageLease { + CherylTguiPrivate::DecodedImage image; + + ~ImageLease() { + if (image.release) + image.release(image.owner); + } + }; +} + +extern "C" CherylTguiPrivate::ImageDecoder cheryl_tgui_exchange_image_decoder_v1(const CherylTguiPrivate::ImageDecoder decoder) noexcept { + return image_decoder.exchange(decoder); +} + +extern "C" bool +cheryl_tgui_restore_image_decoder_v1(CherylTguiPrivate::ImageDecoder expected, const CherylTguiPrivate::ImageDecoder replacement) noexcept { + return image_decoder.compare_exchange_strong(expected, replacement); +} + +namespace tgui { + std::unique_ptr ImageLoader::loadFromFile(const String& filename, Vector2u& image_size) { + std::size_t bytes{}; + const auto data = readFileToMemory(filename, bytes); + return data ? loadFromMemory(data.get(), bytes, image_size) : nullptr; + } + + std::unique_ptr ImageLoader::loadFromMemory(const std::uint8_t* data, const std::size_t bytes, Vector2u& image_size) { + const auto decode = image_decoder.load(); + if (!data || !bytes || !decode) + return {}; + ImageLease lease; + const auto status = decode(data, bytes, lease.image); + if (status == CherylTguiPrivate::DecodeStatus::OutOfMemory) + throw std::bad_alloc{}; + const auto& image = lease.image; + if (status != CherylTguiPrivate::DecodeStatus::Success || !image.pixels || !image.width || !image.height || + image.width > (std::numeric_limits::max)() / 4 / image.height || + image.bytes != static_cast(image.width) * image.height * 4) + return {}; + std::unique_ptr pixels{new std::uint8_t[image.bytes]}; + std::copy_n(image.pixels, image.bytes, pixels.get()); + image_size = {image.width, image.height}; + return pixels; + } +} diff --git a/projects/modules/ui/tgui/src/dependency-contract.h b/projects/modules/ui/tgui/src/dependency-contract.h index 006248a7..b58b8ba2 100644 --- a/projects/modules/ui/tgui/src/dependency-contract.h +++ b/projects/modules/ui/tgui/src/dependency-contract.h @@ -6,8 +6,8 @@ #error Cheryl UI requires the reviewed TGUI 1.13.0 backend contract. #endif -#if !TGUI_HAS_FONT_BACKEND_FREETYPE -#error Cheryl UI requires TGUI custom backend with the FreeType font backend. +#if TGUI_HAS_FONT_BACKEND_FREETYPE +#error Cheryl UI requires the Engine font backend without a second FreeType implementation. #endif #if TGUI_HAS_WINDOW_BACKEND_SFML || TGUI_HAS_WINDOW_BACKEND_SDL || TGUI_HAS_WINDOW_BACKEND_GLFW || TGUI_HAS_WINDOW_BACKEND_RAYLIB || \ diff --git a/projects/modules/ui/tgui/src/font.cpp b/projects/modules/ui/tgui/src/font.cpp new file mode 100644 index 00000000..c666d301 --- /dev/null +++ b/projects/modules/ui/tgui/src/font.cpp @@ -0,0 +1,232 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// TGUI - Texus' Graphical User Interface +// Copyright (C) 2012-2026 Bruno Van de Velde (vdv_b@tgui.eu) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Cheryl adaptation: Engine rasterization with TGUI glyph packing and texture generations. + +#include "font.h" + +#include + +#include +#include +#include + +namespace CE::UI::TGUI::Detail { + CE::Text::FontRasterSize FontBackend::size(const unsigned int character_size) const { + const auto scaled = character_size * m_fontScale; + if (!std::isfinite(scaled) || scaled < 1 || static_cast(scaled) > std::numeric_limits::max()) + return {}; + return {static_cast(scaled)}; + } + + std::optional FontBackend::metrics(const unsigned int character_size) const { + return face_.metrics(size(character_size)); + } + + bool FontBackend::loadFromMemory(std::unique_ptr data, const std::size_t bytes) { + CE::Text::FontRasterizer face; + try { + face = CE::Text::FontRasterizer::load({data.get(), bytes}, 0, 0, true); + } catch (const std::bad_alloc&) { + throw; + } catch (const std::exception& error) { + throw tgui::Exception{error.what()}; + } + const auto scalable = face.properties().scalable; + constexpr unsigned int initial_size = 128; + std::vector pixels(initial_size * initial_size * 4, 255); + for (std::size_t i = 3; i < pixels.size(); i += 4) + pixels[i] = 0; + for (unsigned int y = 0; y < 2; ++y) + for (unsigned int x = 0; x < 2; ++x) + pixels[(y * initial_size + x) * 4 + 3] = 255; + face_ = std::move(face); + scalable_ = scalable; + pixels_ = std::move(pixels); + glyphs_.clear(); + rows_.clear(); + texture_.reset(); + texture_size_ = initial_size; + texture_version_ = 0; + next_row_ = 3; + return true; + } + + bool FontBackend::hasGlyph(const char32_t code) const { + return face_.covers(code); + } + + const FontBackend::Glyph& + FontBackend::glyph(const char32_t code, const unsigned int character_size, const bool bold, const float outline) { + const auto raster_size = size(character_size); + const auto key = constructGlyphKey(code, raster_size.height, bold, outline * m_fontScale); + if (const auto found = glyphs_.find(key); found != glyphs_.end()) + return found->second; + Glyph value; + const auto raster = face_.rasterize(code, raster_size, {true, bold, outline * m_fontScale}); + if (raster) { + value.value.advance = raster->advance; + value.value.bounds = { + static_cast(raster->left), static_cast(-raster->top), static_cast(raster->width), + static_cast(raster->height) + }; + value.left_delta = raster->left_hint_delta; + value.right_delta = raster->right_hint_delta; + if (raster->width && raster->height) { + constexpr unsigned int padding = 2; + auto rect = reserve(raster->width + 2 * padding, raster->height + 2 * padding); + rect.left += padding; + rect.top += padding; + rect.width -= 2 * padding; + rect.height -= 2 * padding; + value.value.textureRect = rect; + for (unsigned int y = 0; y < raster->height; ++y) + for (unsigned int x = 0; x < raster->width; ++x) + pixels_[((rect.top + y) * texture_size_ + rect.left + x) * 4 + 3] = + raster->pixels[static_cast(y) * raster->width + x]; + texture_.reset(); + } + } + return glyphs_.emplace(key, std::move(value)).first->second; + } + + tgui::FontGlyph FontBackend::getGlyph(const char32_t code, const unsigned int character_size, const bool bold, const float outline) { + auto result = glyph(code, character_size, bold, outline).value; + result.advance /= m_fontScale; + result.bounds.setPosition(result.bounds.getPosition() / m_fontScale); + result.bounds.setSize(result.bounds.getSize() / m_fontScale); + return result; + } + + float FontBackend::getKerning(const char32_t left, const char32_t right, const unsigned int character_size, const bool bold) { + if (!left || !right || !face_) + return 0; + const float first_delta = glyph(left, character_size, bold, 0).right_delta; + const float second_delta = glyph(right, character_size, bold, 0).left_delta; + const auto offset = face_.kerning(left, right, size(character_size), true); + if (!scalable_) + return offset * 64 / m_fontScale; + return std::floor(second_delta - first_delta + offset + .5f) / m_fontScale; + } + + float FontBackend::getLineSpacing(const unsigned int character_size) { + const auto value = metrics(character_size); + return value ? value->line_spacing / m_fontScale : 0; + } + + float FontBackend::getFontHeight(const unsigned int character_size) { + const auto value = metrics(character_size); + if (!value) + return 0; + return (scalable_ ? std::ceil(value->design_height) : value->line_spacing) / m_fontScale; + } + + float FontBackend::getAscent(const unsigned int character_size) { + const auto value = metrics(character_size); + return value ? (scalable_ ? std::ceil(value->design_ascent) : value->ascent) / m_fontScale : 0; + } + + float FontBackend::getDescent(const unsigned int character_size) { + const auto value = metrics(character_size); + return value ? (scalable_ ? std::ceil(value->design_descent) : value->descent) / m_fontScale : 0; + } + + float FontBackend::getUnderlinePosition(const unsigned int character_size) { + const auto value = metrics(character_size); + return value ? (scalable_ ? value->underline_position / m_fontScale : character_size / 10.f) : 0; + } + + float FontBackend::getUnderlineThickness(const unsigned int character_size) { + const auto value = metrics(character_size); + return value ? (scalable_ ? value->underline_thickness / m_fontScale : character_size / 14.f) : 0; + } + + std::shared_ptr FontBackend::getTexture(unsigned int, unsigned int& version) { + if (!texture_) { + auto texture = tgui::getBackend()->getRenderer()->createTexture(); + if (!texture->loadTextureOnly({texture_size_, texture_size_}, pixels_.data(), m_isSmooth)) + throw tgui::Exception{U"Could not publish font texture"}; + texture_ = std::move(texture); + ++texture_version_; + } + version = texture_version_; + return texture_; + } + + tgui::Vector2u FontBackend::getTextureSize(unsigned int) { + return {texture_size_, texture_size_}; + } + + void FontBackend::setSmooth(const bool smooth) { + BackendFont::setSmooth(smooth); + if (texture_) + texture_->setSmooth(smooth); + } + + void FontBackend::setFontScale(const float scale) { + if (!std::isfinite(scale) || scale <= 0) + throw tgui::Exception{U"Font scale must be finite and positive"}; + if (m_fontScale == scale) + return; + BackendFont::setFontScale(scale); + texture_.reset(); + } + + tgui::UIntRect FontBackend::reserve(const unsigned int width, const unsigned int height) { + Row* selected{}; + float best_ratio = 0; + for (auto& row : rows_) { + const auto ratio = static_cast(height) / row.height; + if (ratio < .7f || ratio > 1 || width > texture_size_ - row.width || ratio < best_ratio) + continue; + selected = &row; + best_ratio = ratio; + } + if (!selected) { + const auto row_height = height + height / 10; + while (next_row_ + row_height >= texture_size_ || width >= texture_size_) { + if (texture_size_ > std::numeric_limits::max() / 2 || + texture_size_ > std::numeric_limits::max() / texture_size_ / 16) + throw tgui::Exception{U"Font atlas exceeds addressable storage"}; + const auto next_size = texture_size_ * 2; + std::vector pixels(static_cast(next_size) * next_size * 4, 255); + for (std::size_t i = 3; i < pixels.size(); i += 4) + pixels[i] = 0; + for (unsigned int y = 0; y < texture_size_; ++y) + std::copy_n( + pixels_.begin() + static_cast(y) * texture_size_ * 4, texture_size_ * 4, + pixels.begin() + static_cast(y) * next_size * 4 + ); + pixels_ = std::move(pixels); + texture_size_ = next_size; + } + rows_.push_back({0, next_row_, row_height}); + next_row_ += row_height; + selected = &rows_.back(); + } + const tgui::UIntRect result{selected->width, selected->top, width, height}; + selected->width += width; + return result; + } +} diff --git a/projects/modules/ui/tgui/src/font.h b/projects/modules/ui/tgui/src/font.h new file mode 100644 index 00000000..4d918598 --- /dev/null +++ b/projects/modules/ui/tgui/src/font.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include + +#include + +namespace CE::UI::TGUI::Detail { + class FontBackend final : public tgui::BackendFont { + struct Glyph { + tgui::FontGlyph value; + float left_delta{}; + float right_delta{}; + }; + struct Row { + unsigned int width{}; + unsigned int top{}; + unsigned int height{}; + }; + + CE::Text::FontRasterizer face_; + std::unordered_map glyphs_; + std::vector rows_; + std::vector pixels_; + std::shared_ptr texture_; + unsigned int texture_size_{}; + unsigned int texture_version_{}; + unsigned int next_row_ = 3; + bool scalable_{}; + + [[nodiscard]] CE::Text::FontRasterSize size(unsigned int character_size) const; + [[nodiscard]] std::optional metrics(unsigned int character_size) const; + [[nodiscard]] const Glyph& glyph(char32_t code, unsigned int character_size, bool bold, float outline); + [[nodiscard]] tgui::UIntRect reserve(unsigned int width, unsigned int height); + + public: + using BackendFont::loadFromMemory; + [[nodiscard]] bool loadFromMemory(std::unique_ptr data, std::size_t bytes) override; + [[nodiscard]] bool hasGlyph(char32_t code) const override; + [[nodiscard]] tgui::FontGlyph getGlyph(char32_t code, unsigned int size, bool bold, float outline = 0) override; + [[nodiscard]] float getKerning(char32_t left, char32_t right, unsigned int size, bool bold) override; + [[nodiscard]] float getLineSpacing(unsigned int size) override; + [[nodiscard]] float getFontHeight(unsigned int size) override; + [[nodiscard]] float getAscent(unsigned int size) override; + [[nodiscard]] float getDescent(unsigned int size) override; + [[nodiscard]] float getUnderlinePosition(unsigned int size) override; + [[nodiscard]] float getUnderlineThickness(unsigned int size) override; + [[nodiscard]] std::shared_ptr getTexture(unsigned int size, unsigned int& version) override; + [[nodiscard]] tgui::Vector2u getTextureSize(unsigned int size) override; + void setSmooth(bool smooth) override; + void setFontScale(float scale) override; + }; +} diff --git a/projects/modules/ui/tgui/src/image-loader.cpp b/projects/modules/ui/tgui/src/image-loader.cpp new file mode 100644 index 00000000..14a9c469 --- /dev/null +++ b/projects/modules/ui/tgui/src/image-loader.cpp @@ -0,0 +1,39 @@ +#include + +#include "../private/image-decoder.h" + +#include +#include + +namespace { + void release_image(void* owner) noexcept { + delete static_cast(owner); + } + + CherylTguiPrivate::DecodeStatus + decode_image(const std::uint8_t* data, const std::size_t bytes, CherylTguiPrivate::DecodedImage& image) noexcept { + try { + auto decoded = + std::make_unique(CE::Assets::Images::decode(std::span{data, bytes})); + image = {decoded->rgba.data(), decoded->rgba.size(), decoded->size.width, decoded->size.height, decoded.get(), release_image}; + static_cast(decoded.release()); + return CherylTguiPrivate::DecodeStatus::Success; + } catch (const std::bad_alloc&) { + return CherylTguiPrivate::DecodeStatus::OutOfMemory; + } catch (...) { + return CherylTguiPrivate::DecodeStatus::Invalid; + } + } + + struct ImageDecoderRegistration { + CherylTguiPrivate::ImageDecoder previous = cheryl_tgui_exchange_image_decoder_v1(decode_image); + + ~ImageDecoderRegistration() { static_cast(cheryl_tgui_restore_image_decoder_v1(decode_image, previous)); } + }; + + ImageDecoderRegistration decoder_registration; +} + +extern "C" CE_UI_TGUI_API void cheryl_tgui_retain_image_loader() { + static_cast(decoder_registration); +} diff --git a/projects/modules/ui/tgui/src/session.cpp b/projects/modules/ui/tgui/src/session.cpp index 25ba4f5b..918cea1c 100644 --- a/projects/modules/ui/tgui/src/session.cpp +++ b/projects/modules/ui/tgui/src/session.cpp @@ -1,10 +1,10 @@ #include #include "dependency-contract.h" +#include "font.h" #include #include -#include #include #include #include @@ -44,7 +44,7 @@ namespace CE::UI::TGUI { throw Exceptions::failed_operation(CE_HERE, "TGUI already has an active backend/session"); backend_ = std::make_shared(); backend_->setRenderer(std::make_shared(options.maximum_texture_size)); - backend_->setFontBackend(std::make_shared>()); + backend_->setFontBackend(std::make_shared>()); backend_->setFontScale(options.font_scale); tgui::setBackend(backend_); } diff --git a/projects/modules/ui/tgui/tests/consumer/CMakeLists.txt b/projects/modules/ui/tgui/tests/consumer/CMakeLists.txt index 9dacafe4..d3c3fe8b 100644 --- a/projects/modules/ui/tgui/tests/consumer/CMakeLists.txt +++ b/projects/modules/ui/tgui/tests/consumer/CMakeLists.txt @@ -22,3 +22,11 @@ endif() target_link_libraries(${TARGET_TEST_CONSUMER_MODULE_UI_TGUI} PRIVATE ${LINKAGE_PRIVATE_TEST_CONSUMER_MODULE_UI_TGUI}) add_subdirectory(headers) add_dependencies(${TARGET_TEST_CONSUMER_MODULE_UI_TGUI} ${TARGET_TEST_CONSUMER_MODULE_UI_TGUI_HEADERS}) + +add_executable(${TARGET_TEST_CONSUMER_MODULE_UI_TGUI}-native + "${CHERYL_REPOSITORY_ROOT}/.github/ci/sdk-consumer/standalone/native-tgui.cpp") +set_target_properties(${TARGET_TEST_CONSUMER_MODULE_UI_TGUI}-native PROPERTIES + OUTPUT_NAME "${OUTPUT_TEST_CONSUMER_MODULE_UI_TGUI}-native" + RUNTIME_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") +target_link_libraries(${TARGET_TEST_CONSUMER_MODULE_UI_TGUI}-native PRIVATE Cheryl::UI::TGUI) +add_test(NAME ${TARGET_TEST_CONSUMER_MODULE_UI_TGUI}-native COMMAND ${TARGET_TEST_CONSUMER_MODULE_UI_TGUI}-native) diff --git a/projects/modules/ui/tgui/tests/unit/src/decoder.cpp b/projects/modules/ui/tgui/tests/unit/src/decoder.cpp new file mode 100644 index 00000000..4aabccf2 --- /dev/null +++ b/projects/modules/ui/tgui/tests/unit/src/decoder.cpp @@ -0,0 +1,72 @@ +#include + +#include "../../../private/image-decoder.h" + +#include + +#include +#include +#include + +namespace { + bool image_released = false; + constexpr std::array rgba{11, 22, 33, 44}; + + void release(void* owner) noexcept { + *static_cast(owner) = true; + } + + CherylTguiPrivate::DecodeStatus valid(const std::uint8_t*, std::size_t, CherylTguiPrivate::DecodedImage& image) noexcept { + image = {rgba.data(), rgba.size(), 1, 1, &image_released, release}; + return CherylTguiPrivate::DecodeStatus::Success; + } + + CherylTguiPrivate::DecodeStatus invalid(const std::uint8_t* data, std::size_t bytes, CherylTguiPrivate::DecodedImage& image) noexcept { + static_cast(valid(data, bytes, image)); + image.bytes = 3; + return CherylTguiPrivate::DecodeStatus::Success; + } + + CherylTguiPrivate::DecodeStatus + exhausted(const std::uint8_t* data, std::size_t bytes, CherylTguiPrivate::DecodedImage& image) noexcept { + static_cast(valid(data, bytes, image)); + return CherylTguiPrivate::DecodeStatus::OutOfMemory; + } + + struct DecoderLease { + const CherylTguiPrivate::ImageDecoder current; + const CherylTguiPrivate::ImageDecoder previous; + + explicit DecoderLease(const CherylTguiPrivate::ImageDecoder decoder) + : current(decoder), previous(cheryl_tgui_exchange_image_decoder_v1(decoder)) { + image_released = false; + } + + ~DecoderLease() { static_cast(cheryl_tgui_restore_image_decoder_v1(current, previous)); } + }; +} + +TEST(ui_tgui_decoder, callback) { + const DecoderLease decoder(valid); + tgui::Vector2u size; + const auto pixels = tgui::ImageLoader::loadFromMemory(rgba.data(), rgba.size(), size); + ASSERT_TRUE(pixels); + EXPECT_EQ(size, (tgui::Vector2u{1, 1})); + EXPECT_TRUE(std::equal(rgba.begin(), rgba.end(), pixels.get())); + EXPECT_TRUE(image_released); +} + +TEST(ui_tgui_decoder, invalid_cleanup) { + const DecoderLease decoder(invalid); + tgui::Vector2u size{9, 8}; + EXPECT_FALSE(tgui::ImageLoader::loadFromMemory(rgba.data(), rgba.size(), size)); + EXPECT_TRUE(image_released); + EXPECT_EQ(size, (tgui::Vector2u{9, 8})); +} + +TEST(ui_tgui_decoder, allocation_cleanup) { + const DecoderLease decoder(exhausted); + tgui::Vector2u size; + EXPECT_THROW(static_cast(tgui::ImageLoader::loadFromMemory(rgba.data(), rgba.size(), size)), std::bad_alloc); + EXPECT_TRUE(image_released); +} diff --git a/projects/modules/ui/tgui/tests/unit/src/rendering.cpp b/projects/modules/ui/tgui/tests/unit/src/rendering.cpp index d4ec9b5f..26d72047 100644 --- a/projects/modules/ui/tgui/tests/unit/src/rendering.cpp +++ b/projects/modules/ui/tgui/tests/unit/src/rendering.cpp @@ -1,8 +1,11 @@ #include #include +#include #include +#include + #include #include #include @@ -47,6 +50,17 @@ TEST(ui_tgui_texture, owned_pixels) { EXPECT_EQ(first->rgba.front(), 255); } +TEST(ui_tgui_texture, engine_decode) { + constexpr std::array data{0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 32, 0x28, 0, 0, 255, 255, 255, 0, 0, 128}; + tgui::Vector2u size; + const auto pixels = tgui::ImageLoader::loadFromMemory(data.data(), data.size(), size); + ASSERT_TRUE(pixels); + const auto expected = CE::Assets::Images::decode(std::span{data}); + EXPECT_EQ(size, (tgui::Vector2u{1, 2})); + EXPECT_TRUE(std::equal(expected.rgba.begin(), expected.rgba.end(), pixels.get())); + EXPECT_FALSE(tgui::ImageLoader::loadFromMemory(nullptr, 0, size)); +} + TEST(ui_tgui_texture, hit_pixels) { Texture texture(32); auto pixels = std::make_unique(8); diff --git a/projects/modules/ui/tgui/tests/unit/src/session.cpp b/projects/modules/ui/tgui/tests/unit/src/session.cpp index 5fda2c3b..f212d346 100644 --- a/projects/modules/ui/tgui/tests/unit/src/session.cpp +++ b/projects/modules/ui/tgui/tests/unit/src/session.cpp @@ -1,11 +1,13 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -77,6 +79,49 @@ TEST(ui_tgui_session, lifecycle) { EXPECT_TRUE(tgui::isBackendSet()); } +TEST(ui_tgui_session, engine_font) { + Input input; + Session session(input, 7); + const auto font = tgui::Font::getGlobalFont(); + ASSERT_NE(font.getBackendFont(), nullptr); + const auto face = CE::Text::FontRasterizer::load(::defaultFontBytes, 0, 0, true); + const auto expected = face.rasterize(U'A', {24}, {.force_autohint = true}); + ASSERT_TRUE(expected); + const auto plain = font.getGlyph(U'A', 24, false); + const auto bold = font.getGlyph(U'A', 24, true); + const auto outlined = font.getGlyph(U'A', 24, false, 2); + EXPECT_EQ(plain.advance, expected->advance); + EXPECT_EQ(bold.advance, plain.advance + 1); + EXPECT_GT(outlined.bounds.width, plain.bounds.width); + EXPECT_GT(font.getLineSpacing(36), font.getLineSpacing(18)); +} + +TEST(ui_tgui_session, font_height) { + Input input; + Session session(input, 7); + std::vector bytes(std::begin(::defaultFontBytes), std::end(::defaultFontBytes)); + const auto u16 = [&](const std::size_t offset) { return static_cast(bytes[offset]) * 256 + bytes[offset + 1]; }; + const auto u32 = [&](const std::size_t offset) { return u16(offset) * 65536u + u16(offset + 2); }; + std::size_t hhea = 0; + for (unsigned int i = 0; i < u16(4); ++i) { + const auto record = 12 + i * 16; + if (bytes[record] == 'h' && bytes[record + 1] == 'h' && bytes[record + 2] == 'e' && bytes[record + 3] == 'a') + hhea = u32(record + 8); + } + ASSERT_NE(hhea, 0u); + ASSERT_LT(hhea + 8, bytes.size()); + ASSERT_EQ(u16(hhea + 4), 1901u); + // Keep the scalable fixture's 2048 units/em and set descent to -531. + // At 16px, legacy FT_MulFix((1901 - -531), scale)/64 is exactly 19. + // Scaling the two half-grid metrics separately and then subtracting gives + // 19 + 1/64, which incorrectly rounds the font height up to 20. + bytes[hhea + 6] = 0xfd; + bytes[hhea + 7] = 0xed; + const tgui::Font font(bytes.data(), bytes.size()); + ASSERT_NE(font.getBackendFont(), nullptr); + EXPECT_EQ(font.getBackendFont()->getFontHeight(16), 19.f); +} + TEST(ui_tgui_session, routing) { Input input; Session session(input, 7); diff --git a/tools/dependencies/prepare.py b/tools/dependencies/prepare.py new file mode 100644 index 00000000..39ca7b54 --- /dev/null +++ b/tools/dependencies/prepare.py @@ -0,0 +1,77 @@ +"""Prepare shared native dependencies for Cheryl source and SDK builds.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import platform +import subprocess + + +ROOT = Path(__file__).resolve().parents[2] + + +def run(command, *, cwd=ROOT, environment=None, capture=False): + print("+ " + subprocess.list2cmdline(list(map(str, command))), flush=True) + return subprocess.run(command, cwd=cwd, env=environment, check=True, + text=True, capture_output=capture) + + +def dependency_triplet(system, machine): + architecture = {"AMD64": "x64", "x86_64": "x64", "aarch64": "arm64", "ARM64": "arm64"}.get(machine) + if not architecture or system not in {"Linux", "Windows"}: + raise ValueError("Dependency preparation supports Linux and Windows on x64/arm64") + return architecture + ("-windows" if system == "Windows" else "-linux-dynamic") + + +def prepare(vcpkg, installed, *, hwloc=True, native=False, jobs=None): + manifest_path = ROOT / ".github/ci/vcpkg.json" + manifest_bytes = manifest_path.read_bytes() + baseline = json.loads(manifest_bytes)["builtin-baseline"] + if not vcpkg.exists(): + vcpkg.parent.mkdir(parents=True, exist_ok=True) + run(["git", "init", str(vcpkg)]) + run(["git", "remote", "add", "origin", "https://github.com/microsoft/vcpkg.git"], cwd=vcpkg) + run(["git", "fetch", "--depth=1", "origin", baseline], cwd=vcpkg) + run(["git", "checkout", "--detach", baseline], cwd=vcpkg) + revision = run(["git", "rev-parse", "HEAD"], cwd=vcpkg, capture=True).stdout.strip() + if revision != baseline: + raise ValueError(f"Supply vcpkg at pinned revision {baseline}; existing checkout is unchanged") + executable = vcpkg / ("vcpkg.exe" if os.name == "nt" else "vcpkg") + environment = os.environ.copy() + if jobs is not None: + environment["VCPKG_MAX_CONCURRENCY"] = str(jobs) + if not executable.is_file(): + command = (["cmd.exe", "/d", "/c", "bootstrap-vcpkg.bat", "-disableMetrics"] if os.name == "nt" + else ["bash", "bootstrap-vcpkg.sh", "-disableMetrics"]) + run(command, cwd=vcpkg, environment=environment) + triplet = dependency_triplet(platform.system(), platform.machine()) + features = (["hwloc"] if hwloc else []) + (["native"] if native else []) + run([str(executable), "install", f"--triplet={triplet}", + f"--x-manifest-root={manifest_path.parent}", f"--x-install-root={installed}", + "--x-no-default-features", *[f"--x-feature={feature}" for feature in features]], environment=environment) + stamp = {"baseline": baseline, "manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(), + "target_triplet": triplet, "features": features} + (installed / ".cheryl-dependencies.json").write_text(json.dumps(stamp, indent=2) + "\n", encoding="utf-8") + print(f"Shared dependency prefix: {installed / triplet}", flush=True) + print(f"CMake toolchain: {vcpkg / 'scripts/buildsystems/vcpkg.cmake'}", flush=True) + return stamp + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--vcpkg-root", type=Path, default=ROOT / "build/tools/vcpkg") + parser.add_argument("--installed-dir", type=Path, default=ROOT / "build/dependencies") + parser.add_argument("--without-hwloc", action="store_true") + parser.add_argument("--native", action="store_true", help="Prepare HIDAPI for the optional native HID implementation") + parser.add_argument("--jobs", type=int) + args = parser.parse_args() + if args.jobs is not None and args.jobs < 1: + parser.error("--jobs must be positive") + prepare(args.vcpkg_root.resolve(), args.installed_dir.resolve(), hwloc=not args.without_hwloc, + native=args.native, jobs=args.jobs) + + +if __name__ == "__main__": + main() diff --git a/tools/dependencies/prepared.cmake b/tools/dependencies/prepared.cmake new file mode 100644 index 00000000..1b6500cf --- /dev/null +++ b/tools/dependencies/prepared.cmake @@ -0,0 +1,101 @@ +# Use with CMAKE_PROJECT_INCLUDE after preparing the shared dependency prefix. +# Existing application-provided targets take precedence over this convenience integration. +if(NOT PROJECT_NAME STREQUAL "cheryl" AND NOT PROJECT_NAME STREQUAL "cheryl_workers_module") + return() +endif() +set(cheryl_prepared_prefix "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}") +if(NOT IS_DIRECTORY "${cheryl_prepared_prefix}/lib") + message(FATAL_ERROR "The prepared dependency helper requires VCPKG_INSTALLED_DIR and VCPKG_TARGET_TRIPLET") +endif() + +if(PROJECT_NAME STREQUAL "cheryl" AND WIN32 AND + NOT TARGET harfbuzz::harfbuzz AND NOT TARGET harfbuzz) + # The pinned port describes only the .lib as UNKNOWN. Supply its DLL pair + # explicitly so linking and runtime deployment share the same information. + block(SCOPE_FOR VARIABLES) + find_path(cheryl_prepared_harfbuzz_include NAMES hb.h + PATHS "${cheryl_prepared_prefix}/include/harfbuzz" NO_DEFAULT_PATH NO_CACHE REQUIRED) + find_library(cheryl_prepared_harfbuzz_library NAMES harfbuzz + PATHS "${cheryl_prepared_prefix}/lib" NO_DEFAULT_PATH NO_CACHE REQUIRED) + find_file(cheryl_prepared_harfbuzz_runtime NAMES harfbuzz.dll + PATHS "${cheryl_prepared_prefix}/bin" NO_DEFAULT_PATH NO_CACHE REQUIRED) + find_library(cheryl_prepared_harfbuzz_debug_library NAMES harfbuzz + PATHS "${cheryl_prepared_prefix}/debug/lib" NO_DEFAULT_PATH NO_CACHE) + find_file(cheryl_prepared_harfbuzz_debug_runtime NAMES harfbuzz.dll + PATHS "${cheryl_prepared_prefix}/debug/bin" NO_DEFAULT_PATH NO_CACHE) + add_library(cheryl_prepared_harfbuzz SHARED IMPORTED GLOBAL) + add_library(harfbuzz::harfbuzz ALIAS cheryl_prepared_harfbuzz) + set_target_properties(cheryl_prepared_harfbuzz PROPERTIES + IMPORTED_CONFIGURATIONS RELEASE + IMPORTED_LOCATION "${cheryl_prepared_harfbuzz_runtime}" + IMPORTED_LOCATION_RELEASE "${cheryl_prepared_harfbuzz_runtime}" + IMPORTED_IMPLIB "${cheryl_prepared_harfbuzz_library}" + IMPORTED_IMPLIB_RELEASE "${cheryl_prepared_harfbuzz_library}" + INTERFACE_INCLUDE_DIRECTORIES "${cheryl_prepared_harfbuzz_include}") + if(cheryl_prepared_harfbuzz_debug_library AND cheryl_prepared_harfbuzz_debug_runtime) + set_property(TARGET cheryl_prepared_harfbuzz APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG) + set_target_properties(cheryl_prepared_harfbuzz PROPERTIES + IMPORTED_LOCATION_DEBUG "${cheryl_prepared_harfbuzz_debug_runtime}" + IMPORTED_IMPLIB_DEBUG "${cheryl_prepared_harfbuzz_debug_library}") + endif() + endblock() +endif() + +if((NOT DEFINED CHERYL_BUILD_WORKERS OR CHERYL_BUILD_WORKERS) AND + (NOT DEFINED CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC OR CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC) AND + NOT CHERYL_HWLOC_TARGET AND NOT TARGET cheryl_prepared_hwloc) + set(cheryl_prepared_existing_hwloc OFF) + foreach(candidate IN ITEMS hwloc::hwloc HWLOC::hwloc unofficial::hwloc::hwloc hwloc) + if(TARGET "${candidate}") + set(cheryl_prepared_existing_hwloc ON) + endif() + endforeach() + if(NOT cheryl_prepared_existing_hwloc) + block(SCOPE_FOR VARIABLES) + find_path(cheryl_prepared_hwloc_include NAMES hwloc.h + PATHS "${cheryl_prepared_prefix}/include" NO_DEFAULT_PATH NO_CACHE REQUIRED) + find_library(cheryl_prepared_hwloc_library NAMES hwloc libhwloc + PATHS "${cheryl_prepared_prefix}/lib" NO_DEFAULT_PATH NO_CACHE REQUIRED) + find_library(cheryl_prepared_hwloc_debug_library NAMES hwloc libhwloc + PATHS "${cheryl_prepared_prefix}/debug/lib" NO_DEFAULT_PATH NO_CACHE) + set(cheryl_prepared_hwloc_runtime "${cheryl_prepared_hwloc_library}") + set(cheryl_prepared_hwloc_debug_runtime "${cheryl_prepared_hwloc_debug_library}") + if(WIN32) + unset(cheryl_prepared_hwloc_runtime) + unset(cheryl_prepared_hwloc_debug_runtime) + find_file(cheryl_prepared_hwloc_runtime NAMES hwloc.dll hwloc-15.dll libhwloc-15.dll libhwloc.dll + PATHS "${cheryl_prepared_prefix}/bin" NO_DEFAULT_PATH NO_CACHE REQUIRED) + find_file(cheryl_prepared_hwloc_debug_runtime NAMES hwloc.dll hwloc-15.dll libhwloc-15.dll libhwloc.dll + PATHS "${cheryl_prepared_prefix}/debug/bin" NO_DEFAULT_PATH NO_CACHE) + endif() + add_library(cheryl_prepared_hwloc SHARED IMPORTED GLOBAL) + set_target_properties(cheryl_prepared_hwloc PROPERTIES + IMPORTED_CONFIGURATIONS RELEASE + IMPORTED_LOCATION "${cheryl_prepared_hwloc_runtime}" + IMPORTED_LOCATION_RELEASE "${cheryl_prepared_hwloc_runtime}" + INTERFACE_INCLUDE_DIRECTORIES "${cheryl_prepared_hwloc_include}") + if(WIN32) + set_target_properties(cheryl_prepared_hwloc PROPERTIES + IMPORTED_IMPLIB "${cheryl_prepared_hwloc_library}" + IMPORTED_IMPLIB_RELEASE "${cheryl_prepared_hwloc_library}") + endif() + if(cheryl_prepared_hwloc_debug_library AND cheryl_prepared_hwloc_debug_runtime) + set_property(TARGET cheryl_prepared_hwloc APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG) + set_property(TARGET cheryl_prepared_hwloc PROPERTY IMPORTED_LOCATION_DEBUG "${cheryl_prepared_hwloc_debug_runtime}") + if(WIN32) + set_property(TARGET cheryl_prepared_hwloc PROPERTY IMPORTED_IMPLIB_DEBUG "${cheryl_prepared_hwloc_debug_library}") + endif() + endif() + endblock() + set(CHERYL_HWLOC_TARGET cheryl_prepared_hwloc) + endif() +endif() + +if(PROJECT_NAME STREQUAL "cheryl" AND WIN32 AND (NOT DEFINED CHERYL_BUILD_NATIVE_GLFW OR CHERYL_BUILD_NATIVE_GLFW) AND + (NOT DEFINED CHERYL_NATIVE_INPUT OR CHERYL_NATIVE_INPUT) AND NOT TARGET hidapi::hidapi) + # Gainput forces native HID on for Windows. Its later FetchContent request + # reuses this declaration and the prepared package rather than downloading. + include(FetchContent) + FetchContent_Declare(hidapi FIND_PACKAGE_ARGS CONFIG REQUIRED) + FetchContent_MakeAvailable(hidapi) +endif() diff --git a/tools/dependencies/test_prepare.py b/tools/dependencies/test_prepare.py new file mode 100644 index 00000000..064ba061 --- /dev/null +++ b/tools/dependencies/test_prepare.py @@ -0,0 +1,36 @@ +"""Check native preparation policy without installing or building packages.""" + +import importlib.util +from pathlib import Path +from types import SimpleNamespace +import tempfile +import unittest +from unittest.mock import patch + + +SPEC = importlib.util.spec_from_file_location("prepare", Path(__file__).with_name("prepare.py")) +prepare = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(prepare) + + +class PreparationTests(unittest.TestCase): + def test_triplets(self): + self.assertEqual(prepare.dependency_triplet("Windows", "AMD64"), "x64-windows") + self.assertEqual(prepare.dependency_triplet("Linux", "x86_64"), "x64-linux-dynamic") + self.assertEqual(prepare.dependency_triplet("Linux", "aarch64"), "arm64-linux-dynamic") + with self.assertRaises(ValueError): + prepare.dependency_triplet("Linux", "unknown") + + def test_supplied_checkout(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + with patch.object(prepare, "run", return_value=SimpleNamespace(stdout="unrelated-revision\n")) as run: + with self.assertRaisesRegex(ValueError, "existing checkout is unchanged"): + prepare.prepare(root, root / "installed") + self.assertEqual(run.call_count, 1) + self.assertEqual(run.call_args.args[0], ["git", "rev-parse", "HEAD"]) + self.assertFalse((root / "installed").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/input/generate_controls.py b/tools/input/generate_controls.py new file mode 100644 index 00000000..ed747cbe --- /dev/null +++ b/tools/input/generate_controls.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Generate private GLFW/Gainput translations from Cheryl and pinned headers.""" + +import argparse +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[2] +CONTROLS = ROOT / "projects/engine/include/cheryl/core/controls/button-codes.h" +GLFW = ROOT / "extern/glfw/include/GLFW/glfw3.h" +GAINPUT = ROOT / "extern/gainput/lib/include/gainput" +OUTPUT = ROOT / "projects/modules/platform/native-glfw/src/core/controls/generated-controls.inc" + +# Only semantic naming differences need human decisions. Ordinary names and +# contiguous key families come from headers, including keys missing in Gainput. +KEY_NAMES = { + "LeftBracket": "BracketLeft", "RightBracket": "BracketRight", + "GraveAccent": "Grave", "PrintScreen": "Print", "Pause": "Break", + "Backspace": "BackSpace", "Enter": "Return", "KeypadDecimal": "KpDelete", +} +KEYPAD_NAMES = ("Insert", "End", "Down", "PageDown", "Left", "Begin", "Right", "Home", "Up", "PageUp") +MODIFIER_NAMES = {"Shift": "Shift", "Control": "Ctrl", "Alt": "Alt", "Super": "Super"} +PAD_NAMES = { + "LeftTrigger": "Axis4", "RightTrigger": "Axis5", "Back": "Select", "Guide": "Home", + "DPadLeft": "Left", "DPadRight": "Right", "DPadUp": "Up", "DPadDown": "Down", + "LeftShoulder": "L1", "RightShoulder": "R1", "LeftTriggerButton": "L2", "RightTriggerButton": "R2", + "LeftStick": "L3", "RightStick": "R3", +} + + +def enumerators(source, name): + match = re.search(r"enum(?:\s+class)?\s+" + re.escape(name) + r"\b[^{}]*\{([^}]+)\}", source) + if not match: + raise ValueError(f"Cannot find enumeration {name}") + body = re.sub(r"//[^\n]*|/\*.*?\*/", "", match[1], flags=re.S) + return [entry.strip().split("=")[0].strip() for entry in body.split(",") if entry.strip()] + + +def glfw_key(name): + if name.startswith("Digit"): + suffix = name[5:] + elif name.startswith("Keypad"): + suffix = "KP_" + name[6:].upper() + else: + suffix = re.sub(r"([a-z])([A-Z])", r"\1_\2", name).upper() + return "GLFW_KEY_" + suffix + + +def gainput_key(name): + if name.startswith("Digit"): + return "Key" + name[5:] + if name.startswith("Keypad") and name[6:].isdigit(): + return "KeyKp" + KEYPAD_NAMES[int(name[6:])] + if name.startswith(("Left", "Right")): + side = "Left" if name.startswith("Left") else "Right" + modifier = name[len(side):] + if modifier in MODIFIER_NAMES: + return "Key" + MODIFIER_NAMES[modifier] + side[0] + return "Key" + KEY_NAMES.get(name, name.replace("Keypad", "Kp")) + + +def generate(): + controls = CONTROLS.read_text(encoding="utf-8") + native_keys = set(re.findall(r"^#define\s+(GLFW_KEY_\w+)\s+", GLFW.read_text(encoding="utf-8"), re.M)) + gainput_keys = set(enumerators((GAINPUT / "GainputInputDeviceKeyboard.h").read_text(encoding="utf-8"), "Key")) + gainput_pad = set(enumerators((GAINPUT / "GainputInputDevicePad.h").read_text(encoding="utf-8"), "PadButton")) + lines = ["// Generated by tools/input/generate_controls.py; do not edit.", + "// SDK consumers use the committed output and need no generator.", + "constexpr std::array key_mappings{"] + seen = set() + for name in enumerators(controls, "KeyboardKey"): + if name == "Unknown": + continue + native = glfw_key(name) + if native not in native_keys or native in seen: + raise ValueError(f"No unique GLFW mapping for KeyboardKey::{name}") + seen.add(native) + gainput = gainput_key(name) + gainput = "gainput::" + gainput if gainput in gainput_keys else "gainput::InvalidDeviceButtonId" + lines.append(f" KeyMapping{{{native}, CE::Input::KeyboardKey::{name}, {gainput}}},") + ignored = {"GLFW_KEY_UNKNOWN", "GLFW_KEY_LAST", "GLFW_KEY_WORLD_1", "GLFW_KEY_WORLD_2"} + if missing := native_keys - seen - ignored: + raise ValueError(f"GLFW keys need portable identities: {sorted(missing)}") + lines.extend(["};", "", "constexpr std::array pad_mappings{"]) + seen_pad = set() + for name in enumerators(controls, "GamepadControl"): + native = "PadButton" + PAD_NAMES.get(name, name[6:] if name.startswith("Button") else name) + if native in gainput_pad: + seen_pad.add(native) + lines.append(f" PadMapping{{gainput::{native}, CE::Input::GamepadControl::{name}}},") + ignored_pad = {"PadButtonAxisCount_", "PadButtonMax_", "PadButtonCount_"} + if missing := gainput_pad - seen_pad - ignored_pad: + raise ValueError(f"Gainput controls need portable identities: {sorted(missing)}") + lines.extend(["};", ""]) + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="Fail when committed translations need regeneration") + args = parser.parse_args() + generated = generate() + if args.check: + if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != generated: + parser.error("Control translations are stale; run python3 tools/input/generate_controls.py") + else: + OUTPUT.write_text(generated, encoding="utf-8") + + +if __name__ == "__main__": + main() From 3fd5aa369d7b756bc0df66a3a26248dece12a6f7 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 02:42:02 -0700 Subject: [PATCH 32/98] Updates documentation tooling tests for revision cache reuse Replace the obsolete expectation that a Git revision change starts a fresh Sphinx build. Assert that XML and parsed pages remain reusable without a forced HTML rewrite, while retained source links, the revision footer, and revision metadata advance to the current commit. Preserve external dependency links, literal commit text, and timestamps of unaffected HTML pages. Cover rendering invalidation when a pinned source dependency changes. Validation: Python syntax and git diff --check. Build and test execution were not requested and remain deferred. --- docs/generation/tests/test_build.py | 58 ++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/docs/generation/tests/test_build.py b/docs/generation/tests/test_build.py index c40d2934..aec560c3 100644 --- a/docs/generation/tests/test_build.py +++ b/docs/generation/tests/test_build.py @@ -68,6 +68,7 @@ def setUp(self): } self.metadata = { "source_revision": "1" * 40, + "source_dependencies": {}, "dirty": False, "tools": {"python": "fixture", "python_source": "system", "packages": {"Sphinx": "8.2.3"}}, "requirements_sha256": "fixture requirements", @@ -281,14 +282,61 @@ def test_initial_reasons(self): "Doxygen extraction took", "Sphinx rendering took", "Documentation checks took"): self.assertIn(reason, self.last_output) - def test_revision_reason(self): + def test_revision_reuse(self): self.run_build() - self.metadata["source_revision"] = "2" * 40 + self.retain_markers() + previous = self.metadata["source_revision"] + source = "https://github.com/cppcooper/cheryl-engine" + + def page(revision): + return ( + 'Retained API page\n' + f'Header\n' + f"{revision[:12]}\n" + f'Edit on GitHub\n' + f'Dependency\n' + f'
{previous}
\n' + '\n' + ) + + self.html_marker.write_text(page(previous), encoding="utf-8") + unrelated = self.output / "html/unrelated.html" + unrelated.write_text("Unchanged page\n", encoding="utf-8") + os.utime(unrelated, ns=(1_600_000_000_000_000_000, 1_600_000_000_000_000_000)) + unchanged_time = unrelated.stat().st_mtime_ns + revision = "2" * 40 + self.metadata["source_revision"] = revision + + with patch.object(builder, "revision", return_value=revision): + self.assertEqual(self.run_build(), 0) - with patch.object(builder, "revision", return_value=self.metadata["source_revision"]): - self.run_build() + self.assertEqual(self.extractor.call_count, 1) + self.assertEqual(self.sphinx_runs[-1]["environment"], b"environment-1") + self.assertTrue(self.sphinx_runs[-1]["html_retained"]) + self.assertTrue(self.sphinx_runs[-1]["doctrees_retained"]) + self.assertNotIn("-a", self.sphinx_runs[-1]["command"]) + self.assertEqual(self.html_marker.read_text(encoding="utf-8"), page(revision)) + self.assertEqual(self.doctree_marker.read_text(encoding="utf-8"), "Existing doctree\n") + self.assertEqual(unrelated.stat().st_mtime_ns, unchanged_time) + metadata = json.loads((self.output / "html/revision.json").read_text(encoding="utf-8")) + self.assertEqual(metadata["source_revision"], revision) + self.assertEqual(len(self.check_runs), 2) + self.assertIn("Updating Sphinx output incrementally.", self.last_output) - self.assertIn("rendering inputs changed (revision)", self.last_output) + def test_submodule_change(self): + self.metadata["source_dependencies"] = {"external/dependency": "a" * 40} + self.run_build() + self.retain_markers() + self.metadata["source_dependencies"]["external/dependency"] = "b" * 40 + + self.run_build() + + self.assertEqual(self.extractor.call_count, 1) + self.assertIsNone(self.sphinx_runs[-1]["environment"]) + self.assertFalse(self.sphinx_runs[-1]["html_retained"]) + self.assertFalse(self.sphinx_runs[-1]["doctrees_retained"]) + self.assertIn("rendering inputs changed (source_dependencies)", self.last_output) def test_missing_environment(self): self.run_build() From 44dc5473562dfd57e9300132ebe0d4601c553efe Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 03:06:26 -0700 Subject: [PATCH 33/98] Fixes SDK runtime installation policy in generated scripts Both Linux module configurations reach SDK packaging but fail when the generated install script includes the runtime installer without CMP0057 enabled. The IN_LIST expression used to avoid patching the same library twice is then rejected. Capture CMP0057 NEW when defining cheryl_sdk_install_runtimes and restore the surrounding policy scope afterward. The installer can use IN_LIST independently of the calling script, without changing caller policies. Validation: git diff --check and source review against the CMake policy-scope contract. No builds, installation probes, or tests were run. --- cmake/CherylRuntimeDependencies.cmake | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmake/CherylRuntimeDependencies.cmake b/cmake/CherylRuntimeDependencies.cmake index 1b259016..afe83ddf 100644 --- a/cmake/CherylRuntimeDependencies.cmake +++ b/cmake/CherylRuntimeDependencies.cmake @@ -73,6 +73,10 @@ function(cheryl_runtime_dependencies result) cmake_policy(POP) endfunction() +# Generated install scripts do not inherit the project's policy version. +# Functions capture policies at definition, including IN_LIST support. +cmake_policy(PUSH) +cmake_policy(SET CMP0057 NEW) function(cheryl_sdk_install_runtimes destination) cheryl_runtime_dependencies(libraries ${ARGN}) if(NOT libraries) @@ -150,3 +154,4 @@ function(cheryl_sdk_install_runtimes destination) message(FATAL_ERROR "Cannot collect SDK redistribution notices: ${output}${diagnostic}") endif() endfunction() +cmake_policy(POP) From b1db6c1c7a10d5ffcf316b60ae91c69aacda1832 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 03:37:23 -0700 Subject: [PATCH 34/98] Fixes relocated SDK checks at host runtime boundaries Treat dependencies reached only through allowed host interfaces as host-owned. The flattened ldd output previously rejected libcap in the static Linux SDK and libbsd in the shared Linux SDK even though the direct import checks passed. Collect packaged runtime names and every SDK library DT_NEEDED entry before validating loaded paths. Retain relative runtime search path checks, missing dependency failures, and rejection of direct or transitive SDK-owned libraries loaded outside the package. Updates direct-import fixtures and adds focused coverage for host dependency closures and transitive SDK runtime leaks. Python syntax and whitespace checks pass; builds and tests were not run. --- .github/ci/test_verify_sdk.py | 44 +++++++++++++++++++++++++++++++++-- .github/ci/verify_sdk.py | 12 ++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/.github/ci/test_verify_sdk.py b/.github/ci/test_verify_sdk.py index f3868d59..382b3234 100644 --- a/.github/ci/test_verify_sdk.py +++ b/.github/ci/test_verify_sdk.py @@ -45,7 +45,8 @@ def test_external_runtime(self): outside.write_bytes(b"host runtime fixture") def execute(command, **kwargs): - output = ("(RUNPATH) Library runpath: [$ORIGIN]\n" if command[0] == "readelf" + output = ("(RUNPATH) Library runpath: [$ORIGIN]\n(NEEDED) Shared library: [libpng16.so.16]\n" + if command[0] == "readelf" else f"libpng16.so.16 => {outside} (0x123456)\n") return self.result(command, output) @@ -72,9 +73,48 @@ def execute(command, **kwargs): self.assertEqual({Path(command[1]) for command in commands if command[0] == "ldd"}, {self.library, dependency}) self.assertEqual(len(commands), 4) + def test_system_closure(self): + host = self.root / "host libraries" + for interface, dependency in (("libudev.so.1", "libcap.so.2"), ("libXdmcp.so.6", "libbsd.so.0")): + def execute(command, **kwargs): + output = (f"(RUNPATH) Library runpath: [$ORIGIN]\n(NEEDED) Shared library: [{interface}]\n" + if command[0] == "readelf" else + f"{interface} => {host / interface} (0x123456)\n" + f"{dependency} => {host / dependency} (0x234567)\n") + return self.result(command, output) + + with self.subTest(interface=interface), patch.object(verify_sdk.subprocess, "run", side_effect=execute): + verify_sdk.check_relocated_runtimes(self.sdk, {"system": "Linux"}) + + def test_transitive_external(self): + dependency = self.library.with_name("libpng16.so.16") + dependency.write_bytes(b"packaged image runtime") + transitive = self.library.with_name("libz.so.1") + transitive.write_bytes(b"packaged compression runtime") + outside = self.root / "host libraries/libz.so.1" + imports = {self.library: dependency.name, dependency: transitive.name, transitive: "libc.so.6"} + + def execute(command, **kwargs): + library = Path(command[1]) + if command[0] == "readelf": + output = f"(RUNPATH) Library runpath: [$ORIGIN]\n(NEEDED) Shared library: [{imports[library]}]\n" + elif library == self.library: + output = (f"{dependency.name} => {dependency} (0x123456)\n" + f"{transitive.name} => {outside} (0x234567)\n") + elif library == dependency: + output = f"{transitive.name} => {transitive} (0x123456)\n" + else: + output = "libc.so.6 => /system/libc.so.6 (0x123456)\n" + return self.result(command, output) + + with patch.object(verify_sdk.subprocess, "run", side_effect=execute), \ + self.assertRaisesRegex(ValueError, "loads libz.so.1 outside its package"): + verify_sdk.check_relocated_runtimes(self.sdk, {"system": "Linux"}) + def test_missing_runtime(self): def execute(command, **kwargs): - output = ("(RUNPATH) Library runpath: [$ORIGIN]\n" if command[0] == "readelf" + output = ("(RUNPATH) Library runpath: [$ORIGIN]\n(NEEDED) Shared library: [libpng16.so.16]\n" + if command[0] == "readelf" else "libpng16.so.16 => not found\n") return self.result(command, output) diff --git a/.github/ci/verify_sdk.py b/.github/ci/verify_sdk.py index 3089dbcc..c761de22 100644 --- a/.github/ci/verify_sdk.py +++ b/.github/ci/verify_sdk.py @@ -17,12 +17,19 @@ def check_relocated_runtimes(sdk, metadata): if metadata["system"] != "Linux": return - libraries = {path.resolve() for path in (sdk / "lib").glob("*.so*") if path.is_file()} + files = {path for path in (sdk / "lib").glob("*.so*") if path.is_file()} + libraries = {path.resolve() for path in files} + required = {path.name for path in files} for library in sorted(libraries): dynamic = subprocess.run(["readelf", "-d", library], check=True, capture_output=True, text=True).stdout paths = re.findall(r"\((?:RUNPATH|RPATH)\).*?\[(.*?)\]", dynamic) if paths != ["$ORIGIN"]: raise ValueError(f"SDK library requires a relative runtime search path: {library.name}: {paths}") + required.update(re.findall(r"\(NEEDED\).*?\[([^]]+)\]", dynamic)) + # ldd also lists the private dependencies of allowed host interfaces. Those + # remain host-owned, while every packaged runtime and direct SDK import must + # resolve within the SDK unless it is itself an allowed system interface. + for library in sorted(libraries): loaded = subprocess.run(["ldd", library], check=True, capture_output=True, text=True).stdout for line in loaded.splitlines(): match = re.match(r"\s*(\S+)\s+=>\s+(.+)", line) @@ -32,7 +39,8 @@ def check_relocated_runtimes(sdk, metadata): location = re.sub(r"\s+\(0x[0-9a-fA-F]+\)\s*$", "", location).strip() if location == "not found": raise ValueError(f"Relocated SDK cannot load {name} required by {library.name}") - if not system_runtime(name, "Linux") and not Path(location).resolve().is_relative_to(sdk.resolve()): + if (name in required and not system_runtime(name, "Linux") + and not Path(location).resolve().is_relative_to(sdk.resolve())): raise ValueError(f"Relocated SDK loads {name} outside its package: {location}") From 0ceb153fb38450123e42cad2ae01f4da7c68c88d Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 03:48:04 -0700 Subject: [PATCH 35/98] Fixes library argument lookup in SDK relocation test Corrects the subprocess mock used by test_transitive_external. Both readelf -d and ldd receive the library as their final argument; reading argument one treated the readelf option as a filename and raised KeyError before checking runtime resolution in all three Linux tooling jobs. Uses the final command argument so the fixture can exercise the intended transitive runtime rejection. Python syntax and whitespace checks pass; tests were not run locally. --- .github/ci/test_verify_sdk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ci/test_verify_sdk.py b/.github/ci/test_verify_sdk.py index 382b3234..94c17b1f 100644 --- a/.github/ci/test_verify_sdk.py +++ b/.github/ci/test_verify_sdk.py @@ -95,7 +95,7 @@ def test_transitive_external(self): imports = {self.library: dependency.name, dependency: transitive.name, transitive: "libc.so.6"} def execute(command, **kwargs): - library = Path(command[1]) + library = Path(command[-1]) if command[0] == "readelf": output = f"(RUNPATH) Library runpath: [$ORIGIN]\n(NEEDED) Shared library: [{imports[library]}]\n" elif library == self.library: From 3237e032092f89b92fc29222eabb70b5c4a9060f Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 04:10:21 -0700 Subject: [PATCH 36/98] Fixes Windows consumer names in CI tooling checks Compares consumer executable stems in the modules runner assertion so the Windows .exe suffix matches the same eight expected consumers as Linux. The runner already selects the correct platform filename. Python syntax and whitespace checks pass; tests were not run locally. --- .github/ci/test_ci.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ci/test_ci.py b/.github/ci/test_ci.py index c81f9018..3b7f0f9a 100644 --- a/.github/ci/test_ci.py +++ b/.github/ci/test_ci.py @@ -84,7 +84,7 @@ def test_modules(self): pattern = re.compile(next(command for command in self.commands if command[0] == "ctest")[-1]) for owner in ("native-glfw", "opengl", "ui-tgui", "ui-rmlui", "audio-miniaudio", "ui-coexist"): self.assertIsNotNone(pattern.search(f"tests-{owner}.case"), owner) - consumers = [Path(command[0]).name for command in self.commands if len(command) == 1] + consumers = [Path(command[0]).stem for command in self.commands if len(command) == 1] self.assertEqual(len(consumers), 8) self.assertIn("consumer-module-ui-tgui-native", build) self.assertIn("cheryl-ui-tgui-consumer-native", consumers) From 33a55600c2dad35c6150075677f431a2be28a13e Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 04:13:48 -0700 Subject: [PATCH 37/98] Fixes Windows runtime scans at the system library boundary Excludes resolved DLLs under SystemRoot during GET_RUNTIME_DEPENDENCIES traversal instead of filtering the completed list. This stops recursive inspection of Windows implementation details that produced unresolved AzureAttest, HvsiFileTrust, PdmUtilities, WTDSENSOR, wpaxholder and wtdccm imports in both modules jobs. Escapes the SystemRoot path and matches its casing independently of the DLL resolver. Extends the existing classification fixture to check mixed-case system paths, regex metacharacters, adjacent native directories and Linux isolation while retaining unresolved third-party dependency failures. Python syntax and whitespace checks pass. CMake fixture execution and project builds were not run locally. --- .github/ci/test_components.py | 27 ++++++++++++++++++++++++-- cmake/CherylRuntimeDependencies.cmake | 28 ++++++++++++++++----------- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.github/ci/test_components.py b/.github/ci/test_components.py index 69983f76..aae771cd 100644 --- a/.github/ci/test_components.py +++ b/.github/ci/test_components.py @@ -50,6 +50,7 @@ def runtime_classification(self, windows, excluded, bundled): expected_payload = ";".join(bundled) return self.configure(f''' set(WIN32 {"TRUE" if windows else "FALSE"}) +set(ENV{{SystemRoot}} "${{CMAKE_BINARY_DIR}}/Win.dows+[Host]") include("{ROOT.as_posix()}/cmake/CherylRuntimeDependencies.cmake") function(file operation) if(NOT operation STREQUAL "GET_RUNTIME_DEPENDENCIES") @@ -62,7 +63,7 @@ def runtime_classification(self, windows, excluded, bundled): return() endif() cmake_parse_arguments(probe "" "RESOLVED_DEPENDENCIES_VAR;UNRESOLVED_DEPENDENCIES_VAR;CONFLICTING_DEPENDENCIES_PREFIX" - "LIBRARIES;DIRECTORIES;PRE_EXCLUDE_REGEXES" ${{ARGN}}) + "LIBRARIES;DIRECTORIES;PRE_EXCLUDE_REGEXES;POST_EXCLUDE_REGEXES" ${{ARGN}}) set(system_names "{expected_system}") set(payload_names "{expected_payload}") foreach(name IN LISTS system_names payload_names) @@ -78,6 +79,28 @@ def runtime_classification(self, windows, excluded, bundled): message(FATAL_ERROR "Third-party runtime would be omitted: ${{name}}") endif() endforeach() + if(WIN32) + file(TO_CMAKE_PATH "$ENV{{SystemRoot}}" system_root) + set(host_paths "${{system_root}}/System32/SHELL32.dll" "${{system_root}}/kernel32.dll") + string(TOUPPER "${{system_root}}/System32/USER32.dll" alternate_case) + list(APPEND host_paths "${{alternate_case}}") + set(native_paths "${{system_root}}-private/freetype.dll" "${{CMAKE_BINARY_DIR}}/native/harfbuzz.dll") + foreach(library IN LISTS host_paths native_paths) + set(excluded FALSE) + foreach(pattern IN LISTS probe_POST_EXCLUDE_REGEXES) + if(library MATCHES "${{pattern}}") + set(excluded TRUE) + endif() + endforeach() + if(library IN_LIST host_paths AND NOT excluded) + message(FATAL_ERROR "OS imports would be scanned recursively: ${{library}}") + elseif(library IN_LIST native_paths AND excluded) + message(FATAL_ERROR "Third-party runtime path would be omitted: ${{library}}") + endif() + endforeach() + elseif(probe_POST_EXCLUDE_REGEXES) + message(FATAL_ERROR "Windows path exclusions leaked into Linux runtime scanning") + endif() set(${{probe_RESOLVED_DEPENDENCIES_VAR}} "${{payload_names}}" PARENT_SCOPE) set(${{probe_UNRESOLVED_DEPENDENCIES_VAR}} "" PARENT_SCOPE) set(${{probe_CONFLICTING_DEPENDENCIES_PREFIX}}_FILENAMES "" PARENT_SCOPE) @@ -124,7 +147,7 @@ def test_runtime_missing(self): include("{ROOT.as_posix()}/cmake/CherylRuntimeDependencies.cmake") function(file operation) cmake_parse_arguments(probe "" "RESOLVED_DEPENDENCIES_VAR;UNRESOLVED_DEPENDENCIES_VAR" - "LIBRARIES;DIRECTORIES;PRE_EXCLUDE_REGEXES;CONFLICTING_DEPENDENCIES_PREFIX" ${{ARGN}}) + "LIBRARIES;DIRECTORIES;PRE_EXCLUDE_REGEXES;POST_EXCLUDE_REGEXES;CONFLICTING_DEPENDENCIES_PREFIX" ${{ARGN}}) set(${{probe_RESOLVED_DEPENDENCIES_VAR}} "" PARENT_SCOPE) set(${{probe_UNRESOLVED_DEPENDENCIES_VAR}} "libmissing.so.1" PARENT_SCOPE) endfunction() diff --git a/cmake/CherylRuntimeDependencies.cmake b/cmake/CherylRuntimeDependencies.cmake index afe83ddf..8980bb40 100644 --- a/cmake/CherylRuntimeDependencies.cmake +++ b/cmake/CherylRuntimeDependencies.cmake @@ -33,11 +33,25 @@ function(cheryl_runtime_dependencies result) cmake_policy(POP) return() endif() + set(system_paths) if(WIN32) set(system_patterns "^[Aa][Pp][Ii]-[Mm][Ss]-" "^[Ee][Xx][Tt]-[Mm][Ss]-" "^[Ii][Cc][Uu]\\.[Dd][Ll][Ll]$" "^[Oo][Pp][Ee][Nn][Gg][Ll]32\\.[Dd][Ll][Ll]$" "^[Vv][Cc][Rr][Uu][Nn][Tt][Ii][Mm][Ee]" "^[Mm][Ss][Vv][Cc][PpRr]" "^[Cc][Oo][Nn][Cc][Rr][Tt]" "^[Uu][Cc][Rr][Tt][Bb][Aa][Ss][Ee]") + file(TO_CMAKE_PATH "$ENV{SystemRoot}" system_root) + if(system_root) + string(TOLOWER "${system_root}" system_pattern) + string(REGEX REPLACE "/+$" "" system_pattern "${system_pattern}") + string(REGEX REPLACE "([][+.*()^$?|\\\\])" "\\\\\\1" system_pattern "${system_pattern}") + string(REGEX MATCHALL "[a-z]" system_letters "${system_pattern}") + list(REMOVE_DUPLICATES system_letters) + foreach(letter IN LISTS system_letters) + string(TOUPPER "${letter}" upper) + string(REPLACE "${letter}" "[${letter}${upper}]" system_pattern "${system_pattern}") + endforeach() + list(APPEND system_paths "^${system_pattern}/") + endif() else() set(system_patterns "^(ld-linux|ld64|ld-musl)" "^lib(c|m|mvec|dl|pthread|rt|resolv|util|anl|BrokenLocale|nss_[A-Za-z0-9_]+|stdc\\+\\+|gcc_s)\\.so(\\.|$)" @@ -47,6 +61,8 @@ function(cheryl_runtime_dependencies result) file(GET_RUNTIME_DEPENDENCIES LIBRARIES ${libraries} DIRECTORIES ${directories} RESOLVED_DEPENDENCIES_VAR resolved UNRESOLVED_DEPENDENCIES_VAR unresolved PRE_EXCLUDE_REGEXES ${system_patterns} + # Filtering the finished list still follows private Windows imports. + POST_EXCLUDE_REGEXES ${system_paths} CONFLICTING_DEPENDENCIES_PREFIX conflicts) # Source builds can resolve the same DLL beside Engine and in its original # dependency prefix. Copies are compatible only when their bytes match. @@ -57,17 +73,7 @@ function(cheryl_runtime_dependencies result) if(unresolved) message(FATAL_ERROR "Cannot resolve required native runtime dependencies: ${unresolved}") endif() - file(TO_CMAKE_PATH "$ENV{SystemRoot}" system_root) - string(TOLOWER "${system_root}/" system_prefix) - foreach(library IN LISTS resolved) - file(TO_CMAKE_PATH "${library}" normalized) - string(TOLOWER "${normalized}" normalized) - string(FIND "${normalized}" "${system_prefix}" system_location) - if(system_root AND system_location EQUAL 0) - continue() - endif() - list(APPEND libraries "${library}") - endforeach() + list(APPEND libraries ${resolved}) list(REMOVE_DUPLICATES libraries) set(${result} "${libraries}" PARENT_SCOPE) cmake_policy(POP) From 2840431a38c5bc8619f14cc42a2e7356cac49847 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 04:16:27 -0700 Subject: [PATCH 38/98] Updates the missing-runtime fixture for Windows path handling Delegates path conversion in the file-command mock before supplying the unresolved dependency. The Windows scanner now converts SystemRoot before resolving imports, so the failure fixture must permit that setup operation to reach its intended missing-library check. Python syntax and whitespace checks pass; tests were not run locally. --- .github/ci/test_components.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/ci/test_components.py b/.github/ci/test_components.py index aae771cd..1d58a08f 100644 --- a/.github/ci/test_components.py +++ b/.github/ci/test_components.py @@ -146,6 +146,11 @@ def test_runtime_missing(self): result = self.configure(f''' include("{ROOT.as_posix()}/cmake/CherylRuntimeDependencies.cmake") function(file operation) + if(operation STREQUAL "TO_CMAKE_PATH") + _file(TO_CMAKE_PATH "${{ARGV1}}" converted) + set(${{ARGV2}} "${{converted}}" PARENT_SCOPE) + return() + endif() cmake_parse_arguments(probe "" "RESOLVED_DEPENDENCIES_VAR;UNRESOLVED_DEPENDENCIES_VAR" "LIBRARIES;DIRECTORIES;PRE_EXCLUDE_REGEXES;POST_EXCLUDE_REGEXES;CONFLICTING_DEPENDENCIES_PREFIX" ${{ARGN}}) set(${{probe_RESOLVED_DEPENDENCIES_VAR}} "" PARENT_SCOPE) From 091ec2e2537a96a5f08507ebd7d837e6ec3be963 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 04:17:20 -0700 Subject: [PATCH 39/98] Fixes native-only TGUI consumers with shared Linux adapters Retains the shared adapter in an explicit linker input group between push-state, no-as-needed and pop-state. The command-line undefined anchor extracts static archive and import library objects, but an otherwise unreferenced ELF shared adapter can be discarded before its image decoder registration runs. Uses the public TGUI target in the grouped input so source and installed SDK consumers retain the adapter while preserving linker state for other dependencies. Keeps the native consumer free of Cheryl API calls and adds diagnostics for image decoding, pixel contents and texture checks. Whitespace and changed-line formatting checks pass. Builds, native consumers and tests were not run locally; the reported CI failure still requires confirmation on the updated revision. --- .../ci/sdk-consumer/standalone/native-tgui.cpp | 15 +++++++++++++-- projects/modules/ui/tgui/CMakeLists.txt | 8 +++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/ci/sdk-consumer/standalone/native-tgui.cpp b/.github/ci/sdk-consumer/standalone/native-tgui.cpp index c35e5a1d..5c24c5e5 100644 --- a/.github/ci/sdk-consumer/standalone/native-tgui.cpp +++ b/.github/ci/sdk-consumer/standalone/native-tgui.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace { using MemoryLoader = void (tgui::Texture::*)(const std::uint8_t*, std::size_t, const tgui::UIntRect&, const tgui::UIntRect&, bool); @@ -14,10 +15,20 @@ int main() { }; tgui::Vector2u size; const auto pixels = tgui::ImageLoader::loadFromMemory(image.data(), image.size(), size); - if (!pixels || size != tgui::Vector2u{1, 1} || pixels[0] != 255 || pixels[1] != 0 || pixels[2] != 0 || pixels[3] != 255) + if (!pixels) { + std::fputs("Native TGUI consumer: image decoding failed.\n", stderr); return 1; + } + if (size != tgui::Vector2u{1, 1} || pixels[0] != 255 || pixels[1] != 0 || pixels[2] != 0 || pixels[3] != 255) { + std::fputs("Native TGUI consumer: image decoding returned unexpected dimensions or pixels.\n", stderr); + return 1; + } tgui::Texture texture; texture.setColor(tgui::Color::Red); const tgui::Texture copy{texture}; - return memory_loader && copy.getColor() == tgui::Color::Red ? 0 : 1; + if (!memory_loader || copy.getColor() != tgui::Color::Red) { + std::fputs("Native TGUI consumer: texture entry point or color copy check failed.\n", stderr); + return 1; + } + return 0; } diff --git a/projects/modules/ui/tgui/CMakeLists.txt b/projects/modules/ui/tgui/CMakeLists.txt index c89cc57b..548ab427 100644 --- a/projects/modules/ui/tgui/CMakeLists.txt +++ b/projects/modules/ui/tgui/CMakeLists.txt @@ -58,7 +58,7 @@ target_include_directories(${TARGET_LIB_MODULE_UI_TGUI} PUBLIC target_link_libraries(${TARGET_LIB_MODULE_UI_TGUI} PUBLIC Cheryl::Engine TGUI::TGUI) # Native-only authors need the adapter's decoder registration even when no # Session or other Cheryl symbol is referenced. Retain its C ABI anchor before -# static archive scanning and when linking with --as-needed or import libraries. +# static archive or import library scanning. set(tgui_decoder_anchor cheryl_tgui_retain_image_loader) if(WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 4) string(PREPEND tgui_decoder_anchor "_") @@ -70,6 +70,12 @@ elseif(APPLE) else() target_link_options(${TARGET_LIB_MODULE_UI_TGUI} INTERFACE "LINKER:-u,${tgui_decoder_anchor}") endif() +if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CHERYL_BUILD_SHARED_LIBS) + # A command-line undefined symbol does not retain a shared library under + # --as-needed. Keep this adapter without changing the state for other inputs. + target_link_options(${TARGET_LIB_MODULE_UI_TGUI} INTERFACE + "LINKER:SHELL:--push-state --no-as-needed \"$\" --pop-state") +endif() if(CHERYL_BUILD_TESTS) enable_testing() From 7bca5360e58263c944ee873a1e8ce2986234cb2d Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 04:35:16 -0700 Subject: [PATCH 40/98] Revises agent instructions for focused verification and tooling contracts Makes directly related regression maintenance and permitted verification part of an implementation unit. Replaces the blanket test prohibition and consecutive-run guidance with explicit boundaries for lightweight tooling checks, compiler probes, native execution, downloads, documentation generation and resource use. Condenses progression and planning rules while preserving user-directed scope, shared working tree protections, Git restrictions and documentation maintenance requirements. Protects active user builds and caches, and aligns testing-request authorization with the verification policy. Adds guidance for identifying policy ownership across build and SDK boundaries, reviewing affected execution contexts, preventing duplicated classification rules and reporting observed evidence separately from inferred fixes. Reviews the instructions for conflicting authorization rules and passes git diff --check. Only AGENTS.md changes. --- AGENTS.md | 108 ++++++++++++++++++------------------------------------ 1 file changed, 36 insertions(+), 72 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bf1fdca7..e422e5fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,6 @@ ### Work modes and progression -This section applies when in a model mode where multiple sub-agents cant be used frequently. When in such a mode this section provides valuable information worth considering, but documentation (to take an example) should not be deferred. - Each task has one primary work mode: - Review and planning @@ -19,62 +17,23 @@ Each task has one primary work mode: - Documentation audit - Documentation maintenance -Keep independent work objectives separated by mode. Do not automatically transition into another mode merely because related work is discovered or the next phase seems obvious. - -Reading code, inspecting dependencies, checking Git state, and performing other necessary supporting operations do not constitute a mode transition. Keep supporting work proportional to the primary objective. - -#### Progression control - -User-directed progression is the default. Complete the requested work mode and stop at its boundary unless further work has been explicitly authorized. - -When a bounded sequence of work is predictable, reasonably well-defined, and likely to benefit from fewer interruptions, recommend plan-directed progression. Explain the proposed phases and decision checkpoints. Do not adopt the sequence without user approval. +Keep independent work objectives separated by mode. A request for review or investigation does not authorize implementation merely because a fix becomes apparent. -An approved plan may authorize progression across work modes. Complete each phase before advancing to the next, preserving their boundaries and respecting existing authorization restrictions. +A request to implement or fix behavior includes necessary investigation, updates to affected tests, focused regression coverage where useful, and verification permitted below. These supporting activities belong to the same development unit. Independent test design, broader verification campaigns, and documentation work remain separate objectives unless authorized. -Neither progression model requires permission for every coherent subtask within an already authorized phase. +User-directed progression is the default: complete the requested objective and stop before starting an independent objective. When a bounded sequence would benefit from plan-directed progression, propose its phases and decision checkpoints before adopting it. An approved plan may authorize progression across modes without further permission for each coherent subtask. -#### Boundary handling +Stop when: -A boundary is a point where continuing work may require a new decision, a change in scope, or a transition between work modes. Completing a subtask or encountering a dependency does not automatically constitute a stopping point. - -**Continue without interruption when:** - -- Work remains within the authorized mode, scope, and approved plan. -- A necessary dependency can be resolved without exceeding that authorization or introducing an unresolved architectural decision. -- The next subtask requires no additional user decision. -- Supporting operations are necessary to complete the authorized work. - -**Stop at the boundary when:** - -- The requested phase or scope is complete under user-directed progression. -- An approved plan reaches a mandatory decision checkpoint. -- Continuing requires an unauthorized work mode, scope expansion, or operation. -- An unresolved architectural decision, requirement ambiguity, or materially invalidated assumption requires user direction. -- Continuing would depend on unfinished user work or risk modifying protected changes in the shared working tree. +- The requested scope is complete or an approved plan reaches a mandatory decision checkpoint. +- A material requirement, architectural decision, or invalidated assumption requires user direction. +- A necessary operation or scope expansion is not already authorized. +- Continuing would depend on unfinished user work or risk modifying protected changes. - A blocking condition prevents safe or correct completion. -When a boundary is encountered, complete any remaining independent, authorized work that can safely proceed. Report the boundary, its implications, and the recommended next action. - -Advisory checkpoints, including documentation-maintenance recommendations, do not require stopping or independently authorize additional work. +A completed subtask or necessary supporting activity is not itself a stopping point. Continue authorized work that can safely proceed, including independent work unaffected by a blocked portion. Report any boundary, its implications, and the recommended next action. -During plan-directed progression, advance through approved phases without interruption unless a mandatory decision checkpoint or unexpected blocking condition is encountered. - -Existing repository safety and authorization restrictions always take precedence. - -#### Phase completion and task completion - -A work phase and the overall development task are distinct. - -Completing a phase means its authorized objectives have been satisfied or its remaining work has been explicitly identified as blocked or deferred. It does not imply that the broader development task is complete. - -At phase completion: - -- Summarize completed work and any outstanding, blocked, or deferred work. -- Identify the recommended next phase when applicable. -- Recommend documentation maintenance when accumulated changes warrant it, without initiating maintenance automatically. -- Stop under user-directed progression, or continue according to an approved plan-directed sequence. - -Do not declare the overall task complete while known required work remains unfinished. Do not treat optional improvements or deferred documentation maintenance as blockers unless they are explicitly required by the task's acceptance criteria. +At handoff, distinguish implemented changes, observed verification, and outstanding or deferred work. Completing an implementation phase does not establish executable acceptance or complete a broader task with required work remaining. Optional improvements and advisory documentation checkpoints neither block completion nor authorize further work. ### Source authority and context @@ -91,18 +50,21 @@ Prefer concise phase handoffs containing established decisions, relevant constra ## Repository safety - Do not push commits or branches to any remote unless explicitly requested. -- Do not build or compile the project unless explicitly requested. -- Avoid requesting permission to build or test until reaching a blocking point. -- When building is permitted, make an attempt to avoid running consecutive builds. -- Do not run tests unless explicitly requested. -- When testing is permitted, make an attempt to avoid running consecutive tests. -- Instead of consecutive builds and tests, make efforts to achieve the same end result in as few steps as possible. -- If consecutive agent-run builds must be used, then reduce the process priority and or thread count involved to avoid melting the CPU. -- Resource-sharing limits apply to agent-run work. Commands supplied for the user run at normal priority with available parallelism; do not add low-priority wrappers or single-job limits to user-run commands. -- Static analysis and other checks that do not require compilation are permitted. +- Do not build or compile the project or its dependencies unless explicitly authorized by the user's request or an approved plan. +- Preserve active user runs and their outputs. Do not change their configuration, clean their build directories, or invalidate their caches as a routine preparation step. - Do not perform opportunistic refactoring, cleanup, formatting, or API changes merely because they are convenient while working nearby. -- If the task reveals additional work that is related but not required for the current development unit, identify it as discovered work and defer it to an appropriate phase boundary. Incorporate it into the active plan only when it falls within the authorized scope and work mode; otherwise report it for user direction. -- If newly discovered work is required for correctness or for the planned work to proceed, revise the plan within the authorized scope. If it requires an unauthorized work mode, stop at that boundary and request direction unless an approved plan already covers the transition. +- Report related work outside the current development unit as discovered work. Resolve necessary dependencies within the authorized scope; use the progression boundaries above when further work requires user direction. + +## Verification and resource use + +- Run the smallest relevant permitted checks before handing off an implementation. Update affected tests with the behavior they cover, and add focused regression coverage when existing checks do not adequately exercise a material change. +- Static analysis and focused script or tooling tests using mocks or disposable fixtures are permitted without separate authorization when they do not compile project or dependency code, run native project executables, install or download tools or dependencies, generate full documentation, or access devices or external services. CMake script and configuration probes that enable no compiled languages may qualify. +- Inspect an unfamiliar check before executing it, including the subprocesses it launches. A Python filename or a description such as "tooling check" does not establish that a check is lightweight. Configuration that invokes compiler or linker probes requires build authorization. +- Project or dependency builds, native test execution, full documentation generation, dependency or tool downloads or installation, and tests using devices or external services require explicit authorization. Existing authorization persists for relevant fixes and reruns within its scope. When additional authorization is necessary to establish the requested outcome, identify the missing verification and request it together rather than waiting for an unrelated blocker. +- Batch related checks and reuse compatible builds and caches. After fixing a failed check, rerun the affected checks when permitted. Broaden or repeat successful checks only when changed inputs, failures, or unresolved concerns justify it. +- Share machine resources during authorized heavy work by bounding agent-run parallelism or reducing process priority as needed. Commands supplied for the user run at normal priority with available parallelism; do not add low-priority wrappers or single-job limits to user-run commands. +- Test behavior and contracts rather than mirroring implementation details. Use mocks to check orchestration, and distinguish that coverage from executing the generated commands or consuming the resulting artifacts. +- Report what actually ran and what remains unverified. Static checks, mocked tests, and successful configuration do not establish native build, link, relocation, or runtime success. Distinguish confirmed failures, inferred causes, and fixes awaiting executable acceptance. ## Shared working tree @@ -135,17 +97,19 @@ When committing: ## Planning and execution -Before beginning non-trivial implementation work, establish a concise, mode-scoped implementation plan containing coherent tasks and subtasks. Keep the plan in the working context unless a persistent planning artifact is explicitly requested. - -Order the work deliberately so foundational decisions, architectural constraints, risky assumptions, and dependency boundaries are resolved as early as practical. Prefer sequencing that reduces the likelihood of later refactoring, prevents avoidable bugs, and allows later tasks to build on stable completed work. +Before beginning non-trivial implementation work, establish a concise plan containing coherent tasks, risky assumptions, dependency boundaries, and any required decision checkpoints. Keep it in the working context unless a persistent planning artifact is explicitly requested. -Identify boundaries where completing one task may expose additional required work or invalidate an assumption, and structure the plan so those discoveries occur before large amounts of dependent implementation accumulate. +Resolve foundational decisions and uncertain boundaries before accumulating dependent work. Follow the plan within its authorized scope, revising it when implementation reveals material new information. A plan does not authorize operations or independent work outside its approved scope. -Follow the established plan within the authorized work scope unless implementation reveals information that materially changes the design or invalidates part of the plan. +Complete and commit coherent development units independently after appropriate permitted checks. State any outstanding executable acceptance rather than implying that a committed implementation has been fully verified. -A plan does not independently authorize transitions into other work modes. When a new dependency or architectural decision requires work outside that scope, identify the boundary and request direction unless the transition was previously approved. +### Build, tooling, SDK, and CI changes -Complete and commit coherent development units independently so the commit history reflects the progression of the implementation and preserves completed work as later development proceeds. +- Identify the affected contract, its owner, and the path from producer to consumer before changing behavior. Include relevant artifact creation, installation, packaging, relocation, runtime loading, cache reuse, and workflow activation boundaries; inspect only the portions affected by the task. +- Locate the existing authority for each policy before adding another implementation. Prefer extending that owner over duplicating policy in another wrapper, classifier, cache rule, fallback, or compatibility branch. When CMake, Python, or workflow code must express the same policy, keep their semantics aligned and cover their agreement with focused checks. +- Review the execution contexts affected by the change. Examples include project configuration versus generated CMake install or script execution, source versus installed SDK consumption, static versus shared linkage, Linux versus Windows, cold versus warm caches, and default-branch workflow prerequisites. Select relevant coverage; do not run the entire matrix for every edit. +- Preserve the intended contract when correcting a check. Trace dependency ownership before changing bundling rules or system-library exceptions. Distinguish correcting a verifier from changing what the SDK promises to supply. +- For repeated failures, revisit the shared assumption or boundary before adding another local exception. Remove superseded paths when that is part of the requested change; report larger simplification work separately rather than turning a fix into an unrequested redesign. ## Testing requests @@ -153,7 +117,7 @@ Testing-request documentation is maintained only when explicitly authorized. Do Preserve discovered testing requirements in the task handoff until a testing-request maintenance phase is authorized. At that point, reconcile accumulated requirements against existing requests. -Separate test design, test implementation, and test execution according to the authorized work mode. +These rules govern testing-request documentation. They do not defer directly related test maintenance or permitted verification from an implementation task. During authorized testing-request maintenance: @@ -170,7 +134,7 @@ During authorized testing-request maintenance: - Mark requests blocked when fixtures, hardware or an observation harness are unavailable, and identify the next action. Do not invent commands or count skipped, unselected or device-free checks as the missing acceptance. - When the user reports results, reconcile requests and owning plans during the next authorized maintenance phase against the actual tested revision, configuration, platforms and coverage. Remove accepted requests, preserve unresolved portions and repair affected links; retain durable coverage limits in the relevant subject guide rather than appending routine execution logs. -Submitting a request does not authorize agents to configure/build the project or execute tests. Existing repository authorization rules still apply. +Submitting a testing request does not authorize operations that require explicit permission under Verification and resource use. ## Documentation discipline @@ -242,4 +206,4 @@ Do not generate mailbox patches unless explicitly requested. - Keep unit-test names short and intuitive rather than long and descriptive. - Tests must exercise behavior without intentionally emitting compiler warnings in normal builds. Consume results when they matter to the assertion; explicitly discard irrelevant `[[nodiscard]]` results with `static_cast(...)`, including inside exception assertions. Apply the same warning discipline to other intentional diagnostic triggers. - Test compiler diagnostics themselves through isolated probes that capture and assert the expected diagnostic. Keep any required diagnostic suppression narrowly scoped to the tested construct and compiler; do not remove API attributes or disable warnings across a target/project to accommodate a test. -- Do not use "no callers" or "few callers" as grounds for removing an interface. Evaluate interfaces according to their architectural purpose and contract. \ No newline at end of file +- Do not use "no callers" or "few callers" as grounds for removing an interface. Evaluate interfaces according to their architectural purpose and contract. From fa93cf3e894a294fb1c059ef4b4f88b18143e5cb Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 09:19:23 -0700 Subject: [PATCH 41/98] Fixes Windows CI tooling fixtures and expands runner coverage Makes the prepared dependency fixtures select their target platform explicitly so Linux hwloc fixtures do not inherit Windows HarfBuzz discovery and library lookup rules. Exercises Linux shared libraries and Windows DLL/import-library pairs in Release, Debug, and RelWithDebInfo, including incomplete debug pairs and alternate hwloc DLL names. Gives the shared SDK export fixture a Windows HarfBuzz DLL and import library. Checks generated HarfBuzz and hwloc linker paths alongside runtime paths. Adds 432 Linux/Windows runner combinations covering profiles, phases, linkage, hwloc, SDK generation, and build configurations. Covers prepared-prefix environment defaults, MSVC cache flags, invalid job counts, and failure propagation through configure, controls generation, build, CTest, and consumers. Validation: all 45 test_ci.py tests and all 69 test_dependencies.py tests pass locally, including the disposable C++ configuration fixtures. Eight deliberate regressions are rejected by the focused coverage. Native Windows CI acceptance remains pending. --- .github/ci/test_ci.py | 125 +++++++++++++++++++++++++++++++- .github/ci/test_dependencies.py | 82 ++++++++++++++++----- 2 files changed, 186 insertions(+), 21 deletions(-) diff --git a/.github/ci/test_ci.py b/.github/ci/test_ci.py index 3b7f0f9a..fa53e953 100644 --- a/.github/ci/test_ci.py +++ b/.github/ci/test_ci.py @@ -4,6 +4,7 @@ import importlib.util import hashlib import io +from itertools import product import json from pathlib import Path import re @@ -54,12 +55,87 @@ def execute(self, command, source, environment): '', encoding="utf-8") - def invoke(self, *arguments): + def invoke(self, *arguments, environment=None): output = io.StringIO() - with patch.object(ci, "run", self.execute), patch.dict(ci.os.environ, {}, clear=True), redirect_stdout(output): + with patch.object(ci, "run", self.execute), \ + patch.dict(ci.os.environ, environment or {}, clear=True), redirect_stdout(output): ci.main(["--source", str(self.source), "--build-dir", str(self.build), "--jobs", "2", *arguments]) return output.getvalue() + def test_platform_matrix(self): + cases = product(("linux", "win32"), ("headless", "modules", "consumer"), ("all", "build", "test"), + ("static", "shared"), ("ON", "OFF"), (False, True), ("Release", "Debug", "RelWithDebInfo")) + for index, (platform, profile, phase, linkage, hwloc, sdk, configuration) in enumerate(cases): + with self.subTest(platform=platform, profile=profile, phase=phase, linkage=linkage, + hwloc=hwloc, sdk=sdk, configuration=configuration), \ + patch.object(ci.sys, "platform", platform): + self.build = self.root / f"build {index}" + self.commands.clear() + self.environments.clear() + self.invoke("--profile", profile, "--phase", phase, "--linkage", linkage, "--hwloc", hwloc, + "--build-type", configuration, *(["--sdk"] if sdk else [])) + configure = [command for command in self.commands if command[:2] == ["cmake", "-S"]] + builds = [command for command in self.commands if command[:2] == ["cmake", "--build"]] + checks = [command for command in self.commands if command[0] == "ctest"] + controls = [command for command in self.commands if "--check" in command] + consumers = [command for command in self.commands if len(command) == 1] + self.assertEqual(len(configure), int(phase != "test")) + self.assertEqual(len(builds), int(phase != "test")) + self.assertEqual(len(controls), int(phase != "test" and profile == "modules")) + self.assertEqual(len(checks), int(phase != "build" and profile != "consumer")) + if configure: + self.assertIn(f"-DCHERYL_BUILD_SHARED_LIBS={'ON' if linkage == 'shared' else 'OFF'}", configure[0]) + self.assertIn(f"-DCHERYL_BUILD_SDK={'ON' if sdk else 'OFF'}", configure[0]) + self.assertIn(f"-DCHERYL_BUILD_TESTS={'OFF' if profile == 'consumer' else 'ON'}", configure[0]) + self.assertIn(f"-DCHERYL_BUILD_WORKER_TOPOLOGY_HWLOC={hwloc}", configure[0]) + self.assertIn(f"-DCMAKE_BUILD_TYPE={configuration}", configure[0]) + self.assertEqual(configure[0][2:8], + [str(self.source), "-B", str(self.build), "-G", "Ninja", f"-DCMAKE_BUILD_TYPE={configuration}"]) + self.assertEqual("cheryl-sdk" in builds[0], sdk) + self.assertEqual("tests-worker-topology-composition" in builds[0], hwloc == "ON" and profile != "consumer") + if checks: + pattern = re.compile(checks[0][checks[0].index("-R") + 1]) + self.assertIsNotNone(pattern.search("consumer-worker-topology")) + self.assertEqual(bool(pattern.search("tests-worker-topology-composition.case")), hwloc == "ON") + self.assertEqual(bool(pattern.search("tests-ui-coexist.case")), profile == "modules") + self.assertIsNone(pattern.search("acceptance-worker-topology-hwloc.native.binding")) + self.assertIsNone(pattern.search("all-engine.case")) + expected = [] if phase == "build" else ["cheryl-consumer", "cheryl-startup-consumer"] + if phase != "build" and profile == "modules": + expected += ["cheryl-native-glfw-consumer", "cheryl-opengl-consumer", "cheryl-ui-tgui-consumer", + "cheryl-ui-rmlui-consumer", "cheryl-audio-miniaudio-consumer", "cheryl-ui-tgui-consumer-native"] + suffix = ".exe" if platform == "win32" else "" + self.assertEqual(consumers, [[str(self.build / (name + suffix))] for name in expected]) + if platform == "win32": + self.assertTrue(all(environment["PATH"].split(ci.os.pathsep)[0] == str(self.build) + for environment in self.environments)) + + def test_prepared_environment(self): + for platform, profile, hwloc in product(("linux", "win32"), ("headless", "modules", "consumer"), ("ON", "OFF")): + with self.subTest(platform=platform, profile=profile, hwloc=hwloc), patch.object(ci.sys, "platform", platform): + self.commands.clear() + self.environments.clear() + installed = self.root / "prepared with spaces" + vcpkg = self.root / "vcpkg with spaces" + arguments = ["--vcpkg-root", str(vcpkg), "--vcpkg-installed-dir", str(installed)] if platform == "linux" else [] + self.invoke("--profile", profile, "--phase", "build", "--hwloc", hwloc, *arguments, environment={ + "CHERYL_CI_VCPKG_ROOT": str(vcpkg), "CHERYL_CI_VCPKG_INSTALLED_DIR": str(installed), + "CMAKE_CXX_COMPILER_LAUNCHER": "ccache", "PATH": "existing search path"}) + configure = self.commands[0] + features = (["hwloc"] if hwloc == "ON" else []) + (["native"] if platform == "win32" and profile == "modules" else []) + self.assertIn(f"-DVCPKG_MANIFEST_FEATURES={';'.join(features)}", configure) + self.assertIn(f"-DVCPKG_INSTALLED_DIR={installed.as_posix()}", configure) + self.assertIn("-DVCPKG_MANIFEST_INSTALL=OFF", configure) + self.assertIn(f"-DCMAKE_PROJECT_INCLUDE={self.source / 'tools/dependencies/prepared.cmake'}", configure) + self.assertIn(f"-DVCPKG_TARGET_TRIPLET={'x64-windows' if platform == 'win32' else 'x64-linux-dynamic'}", configure) + self.assertEqual("-DCMAKE_POLICY_DEFAULT_CMP0141=NEW" in configure, platform == "win32") + embedded = "-DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=$<$:Embedded>" + self.assertEqual(embedded in configure, platform == "win32") + python = f"-DPython_EXECUTABLE:FILEPATH={Path(ci.sys.executable).resolve().as_posix()}" + self.assertEqual(python in configure, platform == "win32" and profile == "modules") + search = self.environments[0]["PATH"].split(ci.os.pathsep) + self.assertEqual(search, [str(self.build), "existing search path"] if platform == "win32" else ["existing search path"]) + def test_headless(self): output = self.invoke("--profile", "headless", "--hwloc", "ON") command = next(command for command in self.commands if command[0] == "ctest") @@ -169,6 +245,15 @@ def test_preinstalled_requires_root(self): self.assertIn("--vcpkg-installed-dir requires --vcpkg-root", errors.getvalue()) self.assertEqual(self.commands, []) + def test_invalid_jobs(self): + for jobs in ("0", "-1"): + with self.subTest(jobs=jobs), patch("sys.stderr", new_callable=io.StringIO) as errors, \ + self.assertRaises(SystemExit) as raised: + self.invoke("--profile", "headless", "--jobs", jobs) + self.assertEqual(raised.exception.code, 2) + self.assertIn("--jobs must be positive", errors.getvalue()) + self.assertEqual(self.commands, []) + def test_build_failure(self): calls = [] @@ -181,6 +266,42 @@ def fail_build(command, source, environment): ci.main(["--source", str(self.source), "--build-dir", str(self.build), "--profile", "headless"]) self.assertEqual(len(calls), 2) + def test_phase_failures(self): + for platform, stage in product(("linux", "win32"), ("configure", "controls", "build", "ctest", "consumer")): + with self.subTest(platform=platform, stage=stage), patch.object(ci.sys, "platform", platform): + self.commands.clear() + self.build = self.root / f"{platform} {stage}" + suffix = ".exe" if platform == "win32" else "" + failure = { + "configure": lambda command: command[:2] == ["cmake", "-S"], + "controls": lambda command: "--check" in command, + "build": lambda command: command[:2] == ["cmake", "--build"], + "ctest": lambda command: command[0] == "ctest", + "consumer": lambda command: command == [str(self.build / ("cheryl-consumer" + suffix))], + }[stage] + + def execute(command, source, environment): + if failure(list(map(str, command))): + self.commands.append(list(map(str, command))) + raise subprocess.CalledProcessError(7, command) + self.execute(command, source, environment) + + with patch.object(ci, "run", execute), patch.dict(ci.os.environ, {}, clear=True), \ + redirect_stdout(io.StringIO()), self.assertRaises(subprocess.CalledProcessError) as raised: + ci.main(["--source", str(self.source), "--build-dir", str(self.build), "--profile", "modules"]) + self.assertEqual(raised.exception.returncode, 7) + self.assertTrue(failure(self.commands[-1])) + + def test_command_failure(self): + command = ["cmake", "--build", self.build] + environment = {"PATH": "prepared runtime search"} + failure = subprocess.CalledProcessError(3, command) + with patch.object(ci.subprocess, "run", side_effect=failure) as execute, redirect_stdout(io.StringIO()), \ + self.assertRaises(subprocess.CalledProcessError) as raised: + ci.run(command, self.source, environment) + self.assertIs(raised.exception, failure) + execute.assert_called_once_with(command, cwd=self.source, env=environment, check=True) + class SdkPhaseTests(unittest.TestCase): def setUp(self): diff --git a/.github/ci/test_dependencies.py b/.github/ci/test_dependencies.py index 0979f165..2c427f61 100644 --- a/.github/ci/test_dependencies.py +++ b/.github/ci/test_dependencies.py @@ -686,8 +686,8 @@ def test_harfbuzz_imports(self): path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(b"debug fixture") (root / "CMakeLists.txt").write_text(f'''cmake_minimum_required(VERSION 3.28) +set(CMAKE_SYSTEM_NAME Windows) project(cheryl LANGUAGES NONE) -set(WIN32 TRUE) set(CMAKE_FIND_LIBRARY_PREFIXES "") set(CMAKE_FIND_LIBRARY_SUFFIXES .lib) set(VCPKG_INSTALLED_DIR "{(root / 'installed').as_posix()}") @@ -701,14 +701,16 @@ def test_harfbuzz_imports(self): message(FATAL_ERROR "The prepared HarfBuzz DLL/import pair is incomplete") endif() file(GENERATE OUTPUT "${{CMAKE_BINARY_DIR}}/runtime.txt" CONTENT "$") +file(GENERATE OUTPUT "${{CMAKE_BINARY_DIR}}/link.txt" CONTENT "$") ''', encoding="utf-8") for configuration in ("Release", "Debug", "RelWithDebInfo"): build = root / configuration result = subprocess.run(["cmake", "-S", str(root), "-B", str(build), "-G", "Ninja", f"-DCMAKE_BUILD_TYPE={configuration}"], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - directory = "debug/bin" if debug and configuration == "Debug" else "bin" - self.assertEqual(Path((build / "runtime.txt").read_text(encoding="utf-8")), prefix / directory / "harfbuzz.dll") + directory = prefix / "debug" if debug and configuration == "Debug" else prefix + self.assertEqual(Path((build / "runtime.txt").read_text(encoding="utf-8")), directory / "bin/harfbuzz.dll") + self.assertEqual(Path((build / "link.txt").read_text(encoding="utf-8")), directory / "lib/harfbuzz.lib") def test_harfbuzz_supplied(self): helper = Path(__file__).resolve().parents[2] / "tools/dependencies/prepared.cmake" @@ -716,8 +718,8 @@ def test_harfbuzz_supplied(self): root = Path(temporary) (root / "installed/x64-windows/lib").mkdir(parents=True) (root / "CMakeLists.txt").write_text(f'''cmake_minimum_required(VERSION 3.28) +set(CMAKE_SYSTEM_NAME Windows) project(cheryl LANGUAGES NONE) -set(WIN32 TRUE) set(VCPKG_INSTALLED_DIR "{(root / 'installed').as_posix()}") set(VCPKG_TARGET_TRIPLET x64-windows) set(CHERYL_BUILD_WORKERS OFF) @@ -735,43 +737,78 @@ def test_harfbuzz_supplied(self): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) def test_shared_hwloc_closure(self): - self.check_hwloc_configurations(debug=False) + for system in ("Linux", "Windows"): + with self.subTest(system=system): + self.check_hwloc_configurations(system, debug=False) def test_debug_hwloc_closure(self): - self.check_hwloc_configurations(debug=True) + for system in ("Linux", "Windows"): + with self.subTest(system=system): + self.check_hwloc_configurations(system, debug=True) - def check_hwloc_configurations(self, *, debug): + def test_windows_hwloc_debug_pair(self): + for debug in ("library", "runtime"): + with self.subTest(debug=debug): + self.check_hwloc_configurations("Windows", debug=debug) + + def test_windows_hwloc_names(self): + for runtime in ("hwloc-15.dll", "libhwloc-15.dll", "libhwloc.dll"): + with self.subTest(runtime=runtime): + self.check_hwloc_configurations("Windows", debug=True, runtime=runtime) + + def check_hwloc_configurations(self, system, *, debug, runtime=None): helper = Path(__file__).resolve().parents[2] / "tools/dependencies/prepared.cmake" + windows = system == "Windows" + triplet = "x64-windows" if windows else "x64-linux-dynamic" + runtime = runtime or ("hwloc.dll" if windows else "libhwloc.so") + runtime_directory = "bin" if windows else "lib" + library = "hwloc.lib" if windows else "libhwloc.so" + complete_debug = debug is True with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) - prefix = root / "installed/x64-linux-dynamic" + prefix = root / "installed" / triplet (prefix / "include").mkdir(parents=True) (prefix / "lib/pkgconfig").mkdir(parents=True) + (prefix / runtime_directory).mkdir(exist_ok=True) (prefix / "include/hwloc.h").write_bytes(b"fixture") (prefix / "lib/libhwloc.a").write_bytes(b"wrong static fixture") - (prefix / "lib/libhwloc.so").write_bytes(b"shared fixture") + (prefix / "lib" / library).write_bytes(b"shared link fixture") + (prefix / runtime_directory / runtime).write_bytes(b"shared runtime fixture") (prefix / "lib/pkgconfig/hwloc.pc").write_text("Libs: -L${libdir} -lhwloc -lm\nLibs.private: -lpthread\n", encoding="utf-8") - if debug: + if windows: + (prefix / "include/harfbuzz").mkdir() + (prefix / "include/harfbuzz/hb.h").write_bytes(b"fixture") + (prefix / "lib/harfbuzz.lib").write_bytes(b"import fixture") + (prefix / "bin/harfbuzz.dll").write_bytes(b"runtime fixture") + if complete_debug or debug == "library": (prefix / "debug/lib").mkdir(parents=True) - (prefix / "debug/lib/libhwloc.so").write_bytes(b"debug fixture") + (prefix / "debug/lib" / library).write_bytes(b"debug link fixture") + if complete_debug or debug == "runtime": + (prefix / "debug" / runtime_directory).mkdir(parents=True, exist_ok=True) + (prefix / "debug" / runtime_directory / runtime).write_bytes(b"debug runtime fixture") (root / "CMakeLists.txt").write_text(f'''cmake_minimum_required(VERSION 3.28) +set(CMAKE_SYSTEM_NAME {system}) project(cheryl LANGUAGES NONE) +set(CMAKE_FIND_LIBRARY_PREFIXES "{'' if windows else 'lib'}") +set(CMAKE_FIND_LIBRARY_SUFFIXES "{'.lib' if windows else '.so;.a'}") set(VCPKG_INSTALLED_DIR "{(root / 'installed').as_posix()}") -set(VCPKG_TARGET_TRIPLET x64-linux-dynamic) +set(VCPKG_TARGET_TRIPLET {triplet}) set(CHERYL_NATIVE_INPUT OFF) include("{helper.as_posix()}") get_target_property(location cheryl_prepared_hwloc IMPORTED_LOCATION) get_target_property(closure cheryl_prepared_hwloc INTERFACE_LINK_LIBRARIES) get_target_property(configurations cheryl_prepared_hwloc IMPORTED_CONFIGURATIONS) -if(NOT location MATCHES "libhwloc\\\\.so$" OR closure) +if(NOT location STREQUAL "{(prefix / runtime_directory / runtime).as_posix()}" OR closure) message(FATAL_ERROR "Incorrect shared prepared closure: ${{location}} / ${{closure}}") endif() -if(NOT configurations STREQUAL "{'RELEASE;DEBUG' if debug else 'RELEASE'}") +if(NOT configurations STREQUAL "{'RELEASE;DEBUG' if complete_debug else 'RELEASE'}") message(FATAL_ERROR "Incorrect prepared configurations: ${{configurations}}") endif() file(GENERATE OUTPUT "${{CMAKE_BINARY_DIR}}/hwloc.txt" CONTENT "$") +file(GENERATE OUTPUT "${{CMAKE_BINARY_DIR}}/hwloc-link.txt" CONTENT "$") ''', encoding="utf-8") - self.check_generated_locations(root, prefix, debug=debug) + self.check_generated_locations(root, prefix, debug=complete_debug, + runtime=f"{runtime_directory}/{runtime}", library=f"lib/{library}") def test_sdk_shared_export(self): repository = Path(__file__).resolve().parents[2] @@ -800,7 +837,13 @@ def test_sdk_shared_export(self): add_library(glm INTERFACE) add_library(glm::glm-header-only ALIAS glm) add_library(harfbuzz::harfbuzz SHARED IMPORTED) -set_property(TARGET harfbuzz::harfbuzz PROPERTY IMPORTED_LOCATION "${{CMAKE_SOURCE_DIR}}/libharfbuzz.so") +if(WIN32) + set_target_properties(harfbuzz::harfbuzz PROPERTIES + IMPORTED_LOCATION "${{CMAKE_SOURCE_DIR}}/harfbuzz.dll" + IMPORTED_IMPLIB "${{CMAKE_SOURCE_DIR}}/harfbuzz.lib") +else() + set_property(TARGET harfbuzz::harfbuzz PROPERTY IMPORTED_LOCATION "${{CMAKE_SOURCE_DIR}}/libharfbuzz.so") +endif() set_property(GLOBAL PROPERTY CHERYL_SHARED_DEPENDENCIES "harfbuzz::harfbuzz|engine;public_native|engine") target_link_libraries(engine PUBLIC public_native PRIVATE harfbuzz::harfbuzz) include("{(repository / 'cmake/CherylSdk.cmake').as_posix()}") @@ -999,15 +1042,16 @@ def test_sdk_packaged_closure(self): capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - def check_generated_locations(self, root, prefix, *, debug): + def check_generated_locations(self, root, prefix, *, debug, runtime, library): for configuration in ("Release", "Debug", "RelWithDebInfo"): with self.subTest(configuration=configuration): build = root / configuration result = subprocess.run(["cmake", "-S", str(root), "-B", str(build), "-G", "Ninja", f"-DCMAKE_BUILD_TYPE={configuration}"], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - expected = prefix / ("debug/lib/libhwloc.so" if debug and configuration == "Debug" else "lib/libhwloc.so") - self.assertEqual(Path((build / "hwloc.txt").read_text(encoding="utf-8")), expected) + directory = prefix / "debug" if debug and configuration == "Debug" else prefix + self.assertEqual(Path((build / "hwloc.txt").read_text(encoding="utf-8")), directory / runtime) + self.assertEqual(Path((build / "hwloc-link.txt").read_text(encoding="utf-8")), directory / library) if __name__ == "__main__": From 79826ba7769921b1b50f56f2b8262287f570ea00 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 10:01:28 -0700 Subject: [PATCH 42/98] Fixes Windows staging and newline fixtures in CI tooling Uses a POSIX installation prefix for the Linux DESTDIR fixture on every host. Windows drive letters cannot be prepended with a staging directory; the former fixture constructed invalid stageC:/ paths in Windows Check CI tools. Adds a separate Windows DLL installation check that verifies the copied payload without invoking Linux runtime tools. Removes both CR and LF from the checksum fixture seed before testing accepted and rejected line endings. Preserves rejection of malformed CRCRLF input, and writes source snapshot fixtures with explicit UTF-8 bytes so expected archive contents do not depend on host newline defaults. Validation: reproduces the original invalid staging path using Windows Python 3.12.10 and Windows CMake 4.4.3 under Wine. All 32 component checks without compiler probes and 228 other tooling checks pass in that Windows environment. The 32 component checks and all 186 downstream tooling checks pass natively on Linux, including the three Git source snapshot fixtures excluded from Wine. Native hosted Windows CI acceptance remains pending. --- .github/ci/test_components.py | 24 +++++++++++++++++++++++- .github/ci/test_release.py | 6 +++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/ci/test_components.py b/.github/ci/test_components.py index 1d58a08f..06817ac0 100644 --- a/.github/ci/test_components.py +++ b/.github/ci/test_components.py @@ -165,7 +165,8 @@ def runtime_install_probe(self, soname="libfixture.so.2", conflict=False): return self.configure(f''' set(WIN32 FALSE) include("{ROOT.as_posix()}/cmake/CherylRuntimeDependencies.cmake") -set(CMAKE_INSTALL_PREFIX "${{CMAKE_BINARY_DIR}}/prefix") +# This exercises Linux staging even when the host uses Windows drive paths. +set(CMAKE_INSTALL_PREFIX "/prefix") set(ENV{{DESTDIR}} "${{CMAKE_BINARY_DIR}}/stage") set(staged "$ENV{{DESTDIR}}${{CMAKE_INSTALL_PREFIX}}") file(WRITE "${{CMAKE_BINARY_DIR}}/source/libfixture.so.2.7" "runtime bytes") @@ -226,6 +227,27 @@ def test_runtime_soname_path(self): self.assertNotEqual(result.returncode, 0) self.assertIn("Unsafe SDK runtime SONAME: ../escape.so", result.stderr) + def test_windows_runtime_install(self): + result = self.configure(f''' +set(WIN32 TRUE) +unset(ENV{{DESTDIR}}) +include("{ROOT.as_posix()}/cmake/CherylRuntimeDependencies.cmake") +set(CMAKE_INSTALL_PREFIX "${{CMAKE_BINARY_DIR}}/prefix") +file(WRITE "${{CMAKE_BINARY_DIR}}/source/fixture.dll" "runtime bytes") +function(cheryl_runtime_dependencies result) + set(${{result}} "${{ARGN}}" PARENT_SCOPE) +endfunction() +function(execute_process) + message(FATAL_ERROR "Windows runtime installation invoked a Linux tool") +endfunction() +cheryl_sdk_install_runtimes("${{CMAKE_INSTALL_PREFIX}}/bin" "${{CMAKE_BINARY_DIR}}/source/fixture.dll") +file(READ "${{CMAKE_INSTALL_PREFIX}}/bin/fixture.dll" installed) +if(NOT installed STREQUAL "runtime bytes") + message(FATAL_ERROR "The Windows runtime was not installed intact") +endif() +''') + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_shared_runtime(self): result = self.configure(''' add_library(first INTERFACE) diff --git a/.github/ci/test_release.py b/.github/ci/test_release.py index 5b938bf8..fdba5789 100644 --- a/.github/ci/test_release.py +++ b/.github/ci/test_release.py @@ -296,12 +296,12 @@ def test_checksum(self): def test_windows_checksum_lines(self): checksum = self.candidates / "candidate-sdk-windows-modules/SHA256SUMS" - line = checksum.read_bytes().rstrip(b"\n") + line = checksum.read_bytes().rstrip(b"\r\n") for ending in (b"", b"\n", b"\r\n"): with self.subTest(ending=ending): checksum.write_bytes(line + ending) self.assemble() - for ending in (b"\r", b"\n\n", b"\r\nextra", b"\nextra"): + for ending in (b"\r", b"\r\r\n", b"\n\n", b"\r\nextra", b"\nextra"): with self.subTest(ending=ending): checksum.write_bytes(line + ending) self.candidate_rejected("safe archive basename") @@ -644,7 +644,7 @@ def git(self, source, *arguments): def commit(self, source, filename, contents): path = source / filename path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(contents, encoding="utf-8") + path.write_bytes(contents.encode("utf-8")) self.git(source, "add", "--", filename) self.git(source, "commit", "--quiet", "-m", "Adds fixture") return self.git(source, "rev-parse", "HEAD") From a178703951204cbcc68ebfc2b1415b4725aea3c3 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 11:24:33 -0700 Subject: [PATCH 43/98] Fixes aliased temporary paths in CI fixtures Derives the Linux staging fixture DESTDIR from CMake REAL_PATH, matching the runtime patcher that expands Windows 8.3 paths. Prevents an installed and patched runtime from being rejected because RUNNER~1 and runneradmin spell the same directory differently. Adds a directory-alias regression using real staged files and paths containing spaces. The regression reproduces the path comparison failure on Linux before the fix, allowing this assumption to be checked without relying on a hosted Windows temporary directory. Canonicalizes the native-notice fixture root so its fake package ownership database agrees with resolved library lookups. Running downstream tooling checks with an aliased temporary directory exposed this additional failure before CI reached it. Validation: all 33 component checks without compiler probes and all 13 native-notice checks pass with aliased temporary directories on Linux, and with Windows binaries under Wine. The other six downstream suites also pass with aliased Linux temporary directories. Windows API 8.3 path probes succeed, but Wine preserves short names and does not reproduce the hosted NTFS expansion; native Windows CI acceptance remains pending. --- .github/ci/test_components.py | 20 +++++++++++++++++++- .github/ci/test_native_notices.py | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/ci/test_components.py b/.github/ci/test_components.py index 06817ac0..88eecd89 100644 --- a/.github/ci/test_components.py +++ b/.github/ci/test_components.py @@ -4,6 +4,7 @@ import subprocess import tempfile import unittest +from unittest.mock import patch ROOT = Path(__file__).resolve().parents[2] @@ -167,7 +168,9 @@ def runtime_install_probe(self, soname="libfixture.so.2", conflict=False): include("{ROOT.as_posix()}/cmake/CherylRuntimeDependencies.cmake") # This exercises Linux staging even when the host uses Windows drive paths. set(CMAKE_INSTALL_PREFIX "/prefix") -set(ENV{{DESTDIR}} "${{CMAKE_BINARY_DIR}}/stage") +# Match the runtime patcher's canonical paths, including Windows 8.3 names. +file(REAL_PATH "${{CMAKE_BINARY_DIR}}" build_root) +set(ENV{{DESTDIR}} "${{build_root}}/stage") set(staged "$ENV{{DESTDIR}}${{CMAKE_INSTALL_PREFIX}}") file(WRITE "${{CMAKE_BINARY_DIR}}/source/libfixture.so.2.7" "runtime bytes") if({"TRUE" if conflict else "FALSE"}) @@ -217,6 +220,21 @@ def test_runtime_destdir(self): result = self.runtime_install_probe() self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_runtime_alias(self): + with tempfile.TemporaryDirectory(prefix="cheryl-alias-") as directory: + root = Path(directory).resolve() + workspace = root / "real workspace" + workspace.mkdir() + alias = root / "alias workspace" + try: + alias.symlink_to(workspace, target_is_directory=True) + except (NotImplementedError, OSError) as error: + self.skipTest(f"Directory-link fixture unavailable: {error}") + with patch.object(tempfile, "TemporaryDirectory") as temporary: + temporary.return_value.__enter__.return_value = str(alias) + result = self.runtime_install_probe() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_runtime_soname_conflict(self): result = self.runtime_install_probe(conflict=True) self.assertNotEqual(result.returncode, 0) diff --git a/.github/ci/test_native_notices.py b/.github/ci/test_native_notices.py index 40a328c3..e4168a22 100644 --- a/.github/ci/test_native_notices.py +++ b/.github/ci/test_native_notices.py @@ -17,7 +17,7 @@ class NoticeTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory() self.addCleanup(self.temporary.cleanup) - self.root = Path(self.temporary.name) + self.root = Path(self.temporary.name).resolve() self.system = self.root / "system" self.destination = self.root / "sdk/share/licenses/native" self.prefix = self.root / "provider" From c46f7aaf5d92f38e54dca6a7665116b916e8e6ec Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 11:37:20 -0700 Subject: [PATCH 44/98] Adds persistent Windows tooling reuse instructions Records the existing disk-backed Windows tool installation, environment scripts, Wine prefix, and MSVC location in AGENTS.md so new repository sessions can discover and reuse them. Requires preserving reusable tools and cached installers during RAM cleanup, and keeping additional reusable tooling off this workstation memory-backed /tmp. Preserves existing build, test, download, and shared-work restrictions. --- AGENTS.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index e422e5fe..978155f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,6 +66,14 @@ Prefer concise phase handoffs containing established decisions, relevant constra - Test behavior and contracts rather than mirroring implementation details. Use mocks to check orchestration, and distinguish that coverage from executing the generated commands or consuming the resulting artifacts. - Report what actually ran and what remains unverified. Static checks, mocked tests, and successful configuration do not establish native build, link, relocation, or runtime success. Distinguish confirmed failures, inferred causes, and fixes awaiting executable acceptance. +### Local Windows tooling + +- On this workstation, reusable Windows tooling lives in `/mnt/projects/cheryl-windows-ci/`. Check it before downloading tools, creating another Wine environment, or reporting that Windows tooling is unavailable. If it is absent on another host, look for compatible installed tools there. +- The `tools/` directory contains Windows Python, CMake, Ninja, Git, ccache, and vcpkg. Read `env.sh` and `run.sh` for current paths, environment settings, and supported phases before using them; inspect the selected phase before execution. MSVC and the Windows SDK are installed under `/opt/msvc/`. +- Preserve `tools/`, `downloads/`, and `wine-prefix/` across sessions. Store additional reusable tools and isolated Wine prefixes under this disk-backed tooling root, preserving existing versions and active work. +- `/tmp` is memory-backed on this workstation. During RAM cleanup, preserve reusable tools and cached installers; move them to disk-backed storage when needed. Stop idle agent-owned processes and remove disposable test outputs within the authorized scope. +- These locations do not authorize builds, native tests, downloads, or changes to an active environment. The existing verification and shared-work restrictions still apply. + ## Shared working tree The repository is a shared human/agent working tree. Pre-existing uncommitted changes may represent deliberate work in progress and may be incomplete, temporarily inconsistent, non-functional, transitional, preparatory for later refactoring, or difficult to reason about safely. From bd1fa2b73863e5424d79a4c3972fc91ce45e286b Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 16:55:28 -0700 Subject: [PATCH 45/98] Revises HUD and GUI tutorials around TGUI and RmlUi Teaches a TGUI score label and health bar in Create a HUD, and RmlUi documents, RCSS styles, event listeners, focus routing and retained scenes in the interactive GUI guide. Moves direct text replacement and screen placement explanations into First Game and makes the text update recipe self-contained. Consolidates the RmlUi walkthrough and repairs navigation, owner-guide links and descriptions of the existing tutorial executable. Checks local links and heading targets, literalinclude selections, toctrees, public include paths and the example RML structure. The examples have not been compiled or run, and full documentation generation was not performed. --- README.md | 2 +- docs/README.md | 2 +- docs/development/building.md | 2 +- docs/engine-systems/assets.md | 2 +- docs/engine-systems/gui.md | 6 +- docs/engine-systems/text.md | 2 +- docs/getting-started/create-a/hud.md | 284 +++++++++++---- docs/getting-started/create-a/index.md | 12 +- docs/getting-started/first-game.md | 74 +++- docs/getting-started/how-to/create-gui.md | 394 ++++++++++++++------- docs/getting-started/how-to/index.md | 10 +- docs/getting-started/how-to/update-text.md | 138 +++++--- docs/getting-started/project-setup.md | 2 +- docs/gui/index.md | 10 +- projects/apps/tutorial-game/README.md | 19 +- projects/modules/ui/rmlui/README.md | 171 +-------- projects/modules/ui/tgui/README.md | 8 +- 17 files changed, 707 insertions(+), 431 deletions(-) diff --git a/README.md b/README.md index 55273872..31725b13 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ the public aliases to inherit headers and dependencies. | `module_audio_miniaudio` | `Cheryl::Audio::Miniaudio` | Audio decoding, mixing, native output and streaming. | | `demo` | — | Example native application. | | `first-game` | — | Opt-in maintained game from the [walkthrough](docs/getting-started/first-game.md), with its own assets. | -| `tutorial-game` | — | Opt-in Player/Enemy/HUD and interactive GUI example supporting the [tutorials](docs/getting-started/create-a/index.md) and [How-to guides](docs/getting-started/how-to/index.md). | +| `tutorial-game` | — | Opt-in Player/Enemy, direct text score and TGUI controls example supporting the [tutorials](docs/getting-started/create-a/index.md) and [How-to guides](docs/getting-started/how-to/index.md). | | `docs`, `docs-clean`, `docs-tools` | — | Optional incremental generation, full regeneration and shared documentation tool setup; also available through the docs-only CMake project. | | `cheryl-sdk` | — | Builds selected installable libraries when `CHERYL_BUILD_SDK=ON`. | diff --git a/docs/README.md b/docs/README.md index 20e69290..235b55bd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,7 +24,7 @@ Choose an entry point for the work you are doing: - [First game: startup, architecture, sprites, and text](getting-started/first-game.md) - [Create a Player, Enemy or HUD](getting-started/create-a/index.md) - [How to: focused game-development tasks](getting-started/how-to/index.md) -- [Maintained Player/Enemy/HUD and interactive UI example](../projects/apps/tutorial-game/README.md) +- [Maintained Player/Enemy, direct text and TGUI example](../projects/apps/tutorial-game/README.md) ## Engine Systems diff --git a/docs/development/building.md b/docs/development/building.md index 9c6a6ee3..21a3517b 100644 --- a/docs/development/building.md +++ b/docs/development/building.md @@ -78,7 +78,7 @@ dependency-source settings may appear only when their component is selected. | `CHERYL_BUILD_AUDIO_MINIAUDIO` | `ON` | Selects miniaudio decoding, output and streaming. | | `CHERYL_BUILD_DEMO` | `OFF` | Adds `demo` with Native GLFW, OpenGL and native input selected. | | `CHERYL_BUILD_FIRST_GAME` | `OFF` | Adds the maintained `first-game` walkthrough application with Native GLFW, OpenGL and native input selected. | -| `CHERYL_BUILD_TUTORIAL_GAME` | `OFF` | Adds `tutorial-game`, combining Player/Enemy/HUD and six How-to tasks; requires Startup/OpenGL, native input and TGUI. | +| `CHERYL_BUILD_TUTORIAL_GAME` | `OFF` | Adds `tutorial-game` with Player/Enemy, a direct text score and TGUI controls; requires Startup/OpenGL, native input and TGUI. | | `CHERYL_BUILD_DOCS` | `ON` | Adds the optional `docs-tools`, `docs` and `docs-clean` targets; tools are selected when these targets run. | | `CHERYL_BUILD_TESTS` | `OFF` | Adds focused tests and explicitly buildable acceptance/aggregate runners. | | `CHERYL_BUILD_ALL_TESTS` | `OFF` | Includes aggregates in the default build and CTest; requires tests. | diff --git a/docs/engine-systems/assets.md b/docs/engine-systems/assets.md index 1a87cbab..2866072b 100644 --- a/docs/engine-systems/assets.md +++ b/docs/engine-systems/assets.md @@ -50,7 +50,7 @@ its own font-selection/layout path. | Types | Systems | | --- | --- | -| [`Tutorial::Player`](../getting-started/create-a/player.md)
[`Tutorial::Enemy`](../getting-started/create-a/enemy.md)
[`Tutorial::Hud`](../getting-started/create-a/hud.md)
{cpp:struct}`CE::Rendering::DrawPacket2D` | [Rendering](rendering.md)
[Text](text.md) | +| [`Tutorial::Player`](../getting-started/create-a/player.md)
[`Tutorial::Enemy`](../getting-started/create-a/enemy.md)
[`FirstGame::Game`](../getting-started/first-game.md)
{cpp:struct}`CE::Rendering::DrawPacket2D` | [Rendering](rendering.md)
[Text](text.md) | ## Contracts diff --git a/docs/engine-systems/gui.md b/docs/engine-systems/gui.md index 61a233ad..04fa8894 100644 --- a/docs/engine-systems/gui.md +++ b/docs/engine-systems/gui.md @@ -52,7 +52,7 @@ playback. | Types | Systems | | --- | --- | -| [`DemoGame`](../../projects/apps/demo/src/demo-game.h)
[Application-owned UI views](../gui/index.md#construct-widgets-on-the-ui-owner) | [Rendering](rendering.md) | +| [`DemoGame`](../../projects/apps/demo/src/demo-game.h)
[TGUI HUD](../getting-started/create-a/hud.md)
[Application-owned UI views](../gui/index.md#construct-widgets-on-the-ui-owner) | [Rendering](rendering.md) | ## Contracts @@ -97,7 +97,9 @@ session_->request_keyboard_focus(); The application retains `session_` and the callback target. It must also select pointer delivery, feed records and publish the recorded scene. The [GUI construction guide](../gui/index.md) explains those surrounding steps; -[the focused task](../getting-started/how-to/create-gui.md) connects them to a game. +[the HUD tutorial](../getting-started/create-a/hud.md) connects TGUI widgets to a +game, and [the focused GUI task](../getting-started/how-to/create-gui.md) shows +the RmlUi document approach. ## Limits and further reading diff --git a/docs/engine-systems/text.md b/docs/engine-systems/text.md index 0af5297c..0c3f786a 100644 --- a/docs/engine-systems/text.md +++ b/docs/engine-systems/text.md @@ -49,7 +49,7 @@ text path to use. | Types | Systems | | --- | --- | -| [`Tutorial::Hud`](../getting-started/create-a/hud.md)
{cpp:class}`CE::UI::TGUI::Session`
{cpp:class}`CE::UI::RmlUi::Session` | [Rendering](rendering.md)
[GUI](gui.md) | +| [`FirstGame::Game`](../getting-started/first-game.md#put-text-on-screen)
{cpp:class}`CE::UI::TGUI::Session`
{cpp:class}`CE::UI::RmlUi::Session` | [Rendering](rendering.md)
[GUI](gui.md) | ## Contracts diff --git a/docs/getting-started/create-a/hud.md b/docs/getting-started/create-a/hud.md index 1ca292ab..cf934011 100644 --- a/docs/getting-started/create-a/hud.md +++ b/docs/getting-started/create-a/hud.md @@ -1,94 +1,250 @@ -# Create a HUD +# Create a HUD with TGUI -Create an application-owned score display that retains the last complete text -generation while its replacement uploads. This extends the Player/Enemy game -with screen information and safe asynchronous resource replacement. Interactive -widgets are covered separately in [Create an interactive GUI](../how-to/create-gui.md). +Build an application-owned HUD from TGUI widgets: a score label at the top-left +and a health bar at the top-right. Game logic owns the values; the HUD owns +their presentation, layout and retained UI scene. Begin with +[First Game](../first-game.md), then add the Player/Enemy classes if needed. -## Responsibilities and state +First Game explains direct text rendering and +[screen-space placement](../first-game.md#keep-text-in-screen-coordinates). +This tutorial replaces that score display with toolkit widgets. For document +authoring, styling and interactive input through the other UI integration, see +[Create an interactive GUI with RmlUi](../how-to/create-gui.md). -HUD owns font-byte snapshots, its material handle, requested/displayed scores, -one pending upload and the current immutable text generation. Game logic owns -the score's meaning. HUD receives score changes and publishes drawable state; -it does not reach into Player or own the runtime. +## Select TGUI and prepare its materials -Create `hud.h`: +Enable `CHERYL_BUILD_UI_TGUI=ON` in First Game's source configuration, or use an +SDK containing that module. Add the UI target to your game's existing links: -```{literalinclude} ../../../projects/apps/tutorial-game/src/hud.h +```cmake +target_link_libraries(game PRIVATE Cheryl::UI::TGUI) +``` + +Use your executable's target name in place of `game`. Prepare the pinned toolkit +through [Project setup](../project-setup.md); enabling an option does not download +the dependency. + +Copy these application shaders into your asset root's `graphics/shaders/`: +[tgui.vert](../../../assets/graphics/shaders/tgui.vert), +[tgui-solid.frag](../../../assets/graphics/shaders/tgui-solid.frag) and +[tgui-textured.frag](../../../assets/graphics/shaders/tgui-textured.frag). +The existing indexed asset loader does not construct UI materials. + +In your game's `.cpp` file, include +``, ``, `` and +``, then place this helper before the game's method definitions: + +```{literalinclude} ../../../projects/apps/tutorial-game/src/controls.cpp :language: cpp +:start-after: // [materials] +:end-before: // [/materials] ``` -Construction runs on the graphics/platform owner because it uploads the initial -generation. Later `set_score()`/`update()` run on simulation. The dispatched -callback captures prepared data rather than `this`, so accepted work does not -borrow HUD. Font snapshots are retained by layout/prepared values. +This helper creates compatible colored-triangle materials on the graphics +owner. TGUI uses straight alpha blending. Another graphics backend needs its +own compatible builder; the TGUI session has no OpenGL dependency. -## Construct during initialization +## Give the HUD its own class -Replace First Game's `score_` generation member with -`std::optional hud_`, add `int points_ = 0`, and retain a separate -`CE::Rendering::Camera2D hud_camera_`. Include `hud.h`. Keep its existing font -selection/loading and validated `text_` material, then replace initial score -layout/upload with: +Create `hud.h` with the following class. Its constructor saves input/upload +dependencies on platform; the first update creates the toolkit objects on +their simulation owner. ```cpp -hud_.emplace(engine_, std::move(fonts), text_); -hud_camera_.set_framebuffer_size(engine_.window().framebuffer_size()); +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace Tutorial { + class Hud final { + CE::Input::System& input_; + CE::Runtime::PlatformDispatcher::Submission platform_; + CE::UI::TGUI::SceneUploader uploader_; + CE::UI::TGUI::Materials materials_; + CE::UI::TGUI::Scene scene_; + std::future pending_; + std::unique_ptr session_; + tgui::Label::Ptr score_; + tgui::ProgressBar::Ptr health_; + int score_value_ = 0; + unsigned int health_value_ = 100; + + public: + Hud(CE::Runtime::Context& engine, CE::UI::TGUI::Materials materials) + : input_(engine.input()), + platform_(engine.platform_dispatcher().submission()), + uploader_(engine.resources()), + materials_(std::move(materials)) {} + + void set_score(int value) { score_value_ = value; } + void set_health(unsigned int value) { health_value_ = std::min(value, 100u); } + + void update(const CE::Game::TickContext& tick) { + if (!session_) { + session_ = std::make_unique(input_, 2); + session_->set_view(tick.logical_size, tick.framebuffer_size); + create_widgets(); + } else + session_->set_view(tick.logical_size, tick.framebuffer_size); + + session_->handle_input(tick.input.records(), false); + session_->update_time(tick.delta_seconds); + score_->setText(std::format("Score: {}", score_value_)); + health_->setValue(health_value_); + health_->setText(std::format("Health: {}/100", health_value_)); + + static_cast(CE::UI::TGUI::adopt_scene(pending_, scene_)); + if (!pending_.valid()) + pending_ = uploader_.submit(platform_, session_->record(), materials_); + } + + void append(CE::Rendering::FrameWriter& frame) const { scene_.write(frame); } + + void close_after_quiescence() { + score_.reset(); + health_.reset(); + if (session_) { + session_->close_after_quiescence(); + session_.reset(); + } + scene_ = CE::UI::TGUI::Scene{}; + } + + private: + void create_widgets() { + score_ = tgui::Label::create("Score: 0"); + score_->setTextSize(24); + score_->setSize({220, 32}); + session_->gui().add(score_); + session_->set_layout(score_, { + .anchor = {0, 0}, + .offset = {.fixed = {24, 16}}, + }); + + health_ = tgui::ProgressBar::create(); + health_->setSize({220, 32}); + health_->setMaximum(100); + session_->gui().add(health_); + session_->set_layout(health_, { + .anchor = {1, 0}, + .offset = {.fixed = {-24, 16}}, + }); + } + }; +} ``` -Keep the existing camera/input/material initialization. If construction fails, -the optional remains empty and ordinary partial-safe game cleanup applies. +TGUI owns font layout and widget geometry, including the health bar's background +and filled portion. The session supplies its embedded default font through +Cheryl's rasterization service, so this HUD needs no application font file. +Its coordinates are logical UI units with a downward-positive Y axis. + +Add each widget to the GUI before calling `set_layout()`. The label attaches its +top-left corner to the parent's top-left; the bar attaches its top-right corner +to the parent's top-right. Those bindings follow view resizing without manual +framebuffer arithmetic. See [typed layout](../../modules/tgui.md#typed-layout) +for bounded proportional sizes and parent-relative offsets. -## Update content on simulation +Target `2` is a chosen nonzero routing identity. This passive HUD requests no +keyboard focus and selects no pointer delivery, so the game's movement, score +and quit actions remain available. It still feeds the complete ordered input +batch for session bookkeeping. An interactive HUD must explicitly choose its +focus and pointer policy. -After quit handling and the Player/Enemy updates: +TGUI permits one session per process. If the game already has TGUI menus or +controls, put the HUD widgets in that session instead of constructing a second +one. This standalone class assumes it owns the game's only TGUI session. + +## Connect game state to the widgets + +Include `hud.h`, `` and `` in the appropriate game files. +Replace First Game's raw `score_` generation, font/layout/upload block, direct +text draw and generation cleanup with the HUD integration below. Remove its +`text_` material member, lookup, required-resource check and cleanup entry if +the game has no other direct text draws. + +Add these game members: ```cpp -hud_camera_.set_framebuffer_size(tick.framebuffer_size); -if (tick.input.button(next_cell_).pressed()) - hud_->set_score(++points_); -hud_->update(); +std::optional hud_; +int points_ = 0; +unsigned int health_ = 100; ``` -Here `next_cell_` is First Game's existing Space action, repurposed as the -tutorial's score increment. Real game rules decide when to award points. Use an -appropriate bounded score type/policy if the application can approach integer -limits. +During platform-owned `init()`, after graphics initialization: -HUD keeps at most one upload pending and coalesces later changes into the next -request. This example performs small-label CPU preparation on simulation; larger -content can move that stage to a tracked worker group. Ready upload failures -propagate to the runtime's normal failure handling rather than disappearing in -an unread future. +```cpp +hud_.emplace(engine_, materials(engine_.resources(), asset_root_)); +``` -## Publish a screen-space pass +After quit handling and gameplay updates, publish current game values to the +HUD. Here First Game's Space action also increments the example score: -After the world pass containing Player/Enemy: +```cpp +if (tick.input.button(next_cell_).pressed() && points_ < std::numeric_limits::max()) + ++points_; +hud_->set_score(points_); +hud_->set_health(health_); +hud_->update(tick); +``` + +Actual game rules award points and change `health_`. HUD clamps health to its +0–100 display range; it does not decide when damage or healing occurs. + +## Append the retained UI scene + +After the world pass containing the sprite or Player/Enemy draws, in +`prepare_render_frame()`: ```cpp -auto hud_pass = frame.begin_pass(hud_camera_.projection_matrix(), hud_camera_.view_matrix()); -hud_->append(hud_pass, {24.0f, static_cast(hud_camera_.framebuffer_size().height) - 48.0f}); +hud_->append(frame); ``` -The HUD camera keeps its identity view, so world scrolling does not move the -score. It still follows framebuffer resize observations. Its text packets retain -the chosen generation; a later replacement cannot alter an already published -frame. Remove the old direct score draw block to avoid drawing both versions. +The UI scene supplies its own screen projection, so world-camera movement does +not move the HUD. It needs no separate `Camera2D` or manual text draw. The first +scene can be empty until its upload completes. Later updates retain the last +complete scene while at most one replacement uploads; subsequent widget changes +are recorded when that request finishes. Ready upload failures propagate through +the runtime's normal failure handling. + +Record and mutate widgets only on simulation. The upload captures owned scene +data and materials; published frames retain their selected graphics resources +without accessing the live widgets. ## Release and observe -Replace `score_.reset()` with `hud_.reset()` in `deinit()`, before dropping the -game's materials. Runtime settles accepted context work and recycles frames -before that hook. HUD's saved endpoint retains no borrowed context pointer and -its upload captures retain only owned preparation data. - -The score should start at zero and change after Space presses, remain visible -while an upload waits, and stay in screen coordinates as the world view changes. -Repeated unchanged updates should not submit replacement uploads. Run both -runtime modes when checking a game's integration; visual smoothness alone does -not establish correct teardown. - -Read [text updates](../how-to/update-text.md), -[dispatch ownership](../../engine-systems/dispatch.md), -[text contracts](../../assets/text-layout.md) and -[shutdown ordering](../../runtime/runtime-frame-boundary.md#shutdown). +During `deinit()`, after simulation has joined and accepted work has settled: + +```cpp +if (hud_) + hud_->close_after_quiescence(); +hud_.reset(); +``` + +The class drops its external widget handles before session closure. Keep the +input adapter and graphics dependencies alive through this cleanup, including +after partial initialization. + +Expect `Score: 0` and a full health bar. Space advances the score; setting +`health_` to `25` shows a quarter-full bar. Resize the window and move the world +camera: score and health should remain at their screen corners, while gameplay +keys continue to work. Check both runtime modes for input and teardown behavior. + +The current [tutorial-game executable](../../../projects/apps/tutorial-game/README.md) +uses a direct text score and a separate TGUI controls panel; it does not implement +this widget HUD. Its controls source supplies the material helper above. +The [TGUI demo](../../../projects/apps/demo/src/tgui-demo.cpp) also demonstrates +interactive widgets. Read [TGUI contracts](../../modules/tgui.md) and +[GUI ownership](../../gui/index.md) for the integration boundaries. diff --git a/docs/getting-started/create-a/index.md b/docs/getting-started/create-a/index.md index 9aa907e2..ca1d7997 100644 --- a/docs/getting-started/create-a/index.md +++ b/docs/getting-started/create-a/index.md @@ -6,18 +6,18 @@ behavior, state and lifetime. Begin with the maintained [First Game](../first-game.md), keeping its initialization/update/frame/cleanup structure as the integration point. -The [maintained tutorial game](../../../projects/apps/tutorial-game/README.md) -combines these classes and all six focused tasks in one example target. Its -headers are included directly in the tutorials, so class code and -the teaching fixture share one source. First Game remains the introductory -walkthrough; the larger example adds class organization and interactive UI. +Player and Enemy include their headers from the +[maintained tutorial game](../../../projects/apps/tutorial-game/README.md). +The HUD guide provides a TGUI widget class that extends First Game; the +[interactive GUI guide](../how-to/create-gui.md) teaches RmlUi documents. +First Game owns the direct text rendering walkthrough. ```{toctree} :maxdepth: 1 Player Enemy -HUD +HUD with TGUI ``` A Player responds to semantic actions, an Enemy owns autonomous behavior and a diff --git a/docs/getting-started/first-game.md b/docs/getting-started/first-game.md index 25de8673..5c619d0e 100644 --- a/docs/getting-started/first-game.md +++ b/docs/getting-started/first-game.md @@ -6,10 +6,10 @@ The score stays at zero while you establish the application and resource flow. Start with setup and `main()`, then learn which parts of the engine your game uses and who owns its state. The sprite and text walkthroughs apply those rules -from initialization through drawing and cleanup. All excerpts come from the -maintained [first-game application](../../projects/apps/first-game/); they are -parts of the same implementation, with the enclosing function and thread named -above each excerpt. +from initialization through drawing and cleanup. The main walkthrough excerpts +come from the maintained [first-game application](../../projects/apps/first-game/), +with the enclosing function and thread named above each excerpt. The text +replacement and screen-coordinate sections explain extensions to that example. ## Setup @@ -514,10 +514,28 @@ adopt the completed replacement generation. Changing the original string alone does not update an uploaded label. For a small initial label, synchronous `init()` is enough. For text that changes -during play, use the [background-loading handoff](#load-or-replace-resources-during-play) -below: prepare CPU data, dispatch upload to the graphics owner, then adopt its -result during `update()`. Keep the previous generation while the replacement is -pending; published frames retain the version they already selected. +during play, retain the font collection and requested/displayed values with the +game, plus one pending upload future: + +1. Game rules change the requested score. Skip replacement while it matches the + displayed score. +2. When no upload is pending, lay out and prepare the latest requested value on + simulation, then submit owned prepared data to the platform dispatcher. +3. Keep drawing the current `score_` while the replacement uploads. Check the + future without waiting; a ready exception follows the game's failure policy. +4. Adopt a successful generation during `update()`. If the requested value + changed while upload waited, prepare the latest value next. + +This bounds the queue and coalesces intermediate changes. The upload callback +returns a generation instead of capturing `this` to mutate live game state. +Prepared/layout values retain their font snapshots, and each published frame +retains the generation it selected. Small-label preparation can run on +simulation; move expensive CPU preparation to a tracked worker group while +keeping upload on graphics. + +[Display and update text](how-to/update-text.md) supplies the complete member, +initialization, update and cleanup changes. That guide extends this raw text +path; the [TGUI HUD tutorial](create-a/hud.md) teaches toolkit widgets. ### Prepare a frame: place and publish the label @@ -540,6 +558,46 @@ retains the selected resources and copies parameters into the frame. After the hook returns, the runtime publishes the complete frame for the platform renderer to draw and present. +### Keep text in screen coordinates + +The example's camera has an identity view, so the sprite and text can share one +pass. If a later game pans the world view, give direct screen text a separate +`CE::Rendering::Camera2D hud_camera_` member and keep its identity view. +Initialize its size during platform-owned `init()`: + +```cpp +hud_camera_.set_framebuffer_size(engine_.window().framebuffer_size()); +``` + +Keep the copied size current during `update()`: + +```cpp +hud_camera_.set_framebuffer_size(tick.framebuffer_size); +``` + +Replace the existing direct text draw with a separate pass after the world draw +in `prepare_render_frame()`: + +```cpp +auto text_pass = frame.begin_pass(hud_camera_.projection_matrix(), hud_camera_.view_matrix()); +const CE::Rendering::Submission::Context2D text_context{ + text_pass.semantics(), text_pass.parameters(), text_pass.constraints(), + CE::Rendering::Submission::ImageParameter2D{"image", 0} +}; +CE::Rendering::DrawStyle2D text_style; +text_style.material = text_; +text_style.model_matrix = + glm::translate(glm::mat4(1.0f), glm::vec3(24.0f, static_cast(hud_camera_.framebuffer_size().height) - 48.0f, 0.0f)); +text_pass.add(CE::Rendering::Submission::resolve_text(*score_, text_style, text_context)); +``` + +The label remains 24 pixels from the left and 48 below the framebuffer's top +while the world camera moves. Resize observations update the projection and +placement. The separate camera changes where text is drawn; text content still +uses the generation replacement flow above. Toolkit scenes provide their own +screen projection, as shown in [Create a HUD with TGUI](create-a/hud.md) and +[Create an interactive GUI with RmlUi](how-to/create-gui.md). + ### Deinit: release the uploaded label `score_.reset()` and `text_.reset()` release this game's generation and text diff --git a/docs/getting-started/how-to/create-gui.md b/docs/getting-started/how-to/create-gui.md index 2ef9fe49..f91c90f3 100644 --- a/docs/getting-started/how-to/create-gui.md +++ b/docs/getting-started/how-to/create-gui.md @@ -1,119 +1,263 @@ -# Create an interactive GUI +# Create an interactive GUI with RmlUi -Add a TGUI button and edit field to the game, route keyboard/pointer input and -publish a retained UI scene. Start with [First Game](../first-game.md). This task -uses native TGUI widgets and Cheryl's existing session/uploader interfaces; the -[RmlUi owner guide](../../modules/rmlui.md) describes the alternative document -authoring path. +Add a styled RML document with an editable player name and an Apply button. +Connect its event listener to game state, route keyboard/pointer input, and +publish a retained UI scene. Start with [First Game](../first-game.md). +For the C++ widget approach, [Create a HUD with TGUI](../create-a/hud.md) +uses the other UI integration. -## Select the module and prepare materials +## Select the module and prepare assets -Enable/link `Cheryl::UI::TGUI` alongside the native graphics setup. Include: +Enable `CHERYL_BUILD_UI_RMLUI=ON` in your source configuration, or consume an +SDK containing that module. Add it to the game's existing target links: + +```cmake +target_link_libraries(game PRIVATE Cheryl::UI::RmlUi) +``` + +Use your executable's target name in place of `game`. Prepare the pinned +dependency through [Project setup](../project-setup.md). TGUI is optional for +this guide; a game using the TGUI HUD can select both modules. + +Add these files under First Game's asset root: + +```text +assets/ +├── fonts/ +│ ├── DejaVuSans.ttf +│ └── DejaVuSansLicense.txt +└── graphics/ + ├── shaders/ + │ ├── tgui.vert + │ ├── tgui-solid.frag + │ └── tgui-textured.frag + └── ui/ + ├── tutorial.rml + └── tutorial.rcss +``` + +Copy the [font](../../../assets/fonts/DejaVuSans.ttf) and +[its license](../../../assets/fonts/DejaVuSansLicense.txt), and the +[vertex](../../../assets/graphics/shaders/tgui.vert), +[solid](../../../assets/graphics/shaders/tgui-solid.frag) and +[textured](../../../assets/graphics/shaders/tgui-textured.frag) shaders from the +checkout. The shader filenames are shared with the TGUI integration; RmlUi uses +its own materials and premultiplied blending. + +The existing First Game assets are still used. RmlUi loads its documents, styles +and fonts explicitly, outside the graphics manifest loader. + +## Author the document and stylesheet + +Create `graphics/ui/tutorial.rml`: + +```xml + + + Player settings + + + +
+

Player settings

+
Player name
+ + +
+ +
+``` + +Create `graphics/ui/tutorial.rcss`: + +```css +body { + width: 100%; height: 100%; margin: 0; + font-family: tutorial; font-size: 18px; color: #edf2fa; +} +#panel { + position: absolute; left: 16px; top: 16px; + width: 65%; min-width: 280px; max-width: 420px; + height: 200px; box-sizing: border-box; padding: 16px; + background-color: #26394be8; overflow: hidden; +} +h1 { font-size: 24px; margin: 0 0 12px; } +input { + display: block; width: 100%; height: 32px; + box-sizing: border-box; padding: 6px; margin: 4px 0 12px; + background-color: #edf2fa; color: #172532; +} +button { + display: inline-block; padding: 7px 12px; + background-color: #477989; color: #ffffff; +} +button:hover { background-color: #5b92a0; } +button:active { background-color: #365b6b; } +``` + +RML supplies native elements; RCSS supplies their layout and appearance. The +stylesheet path resolves relative to the document. The `tutorial` font family +matches the explicit registration below. UI coordinates use logical window +units with a downward-positive Y axis; the adapter handles framebuffer scaling. +The panel follows the window within its width bounds. + +This example uses rectangular drawing and clipping. Check the +[RmlUi capability contract](../../modules/rmlui.md#recording-and-publication) +before adding transforms, masks or effects. + +## Prepare compatible materials on platform + +Add these includes to the appropriate game header/source files: ```cpp #include -#include -#include -#include -#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include #include #include #include +#include +#include #include #include ``` -The teaching game's UI material builder requires these shaders under your asset root's -`graphics/shaders/` directory: -[tgui.vert](../../../assets/graphics/shaders/tgui.vert), -[tgui-solid.frag](../../../assets/graphics/shaders/tgui-solid.frag) and -[tgui-textured.frag](../../../assets/graphics/shaders/tgui-textured.frag). -Copy those three files when extending First Game's smaller asset set. This -builder is a manual pipeline path; the asset index does not load it automatically. - -Use the maintained teaching game's platform-owned material construction helper: +Place this helper in your game's `.cpp` file before its method definitions: -```{literalinclude} ../../../projects/apps/tutorial-game/src/controls.cpp -:language: cpp -:start-after: // [materials] -:end-before: // [/materials] +```cpp +CE::UI::RmlUi::Materials make_rmlui_materials( + CE::Rendering::Resources::Provider& provider, const std::filesystem::path& root +) { + auto* native = dynamic_cast(&provider); + if (!native) + throw std::runtime_error("This UI material builder requires OpenGL"); + CE::Rendering::Pipelines::Definition definition; + definition.vertex_layout = CE::Rendering::Geometry::Layout2D::Position3UV2Color4; + definition.topology = CE::Rendering::Geometry::Topology::Triangles; + definition.state = { + CE::Rendering::Pipelines::Blend::PremultipliedAlpha, CE::Rendering::Pipelines::Depth::Disabled, false, + CE::Rendering::Pipelines::Cull::None + }; + definition.parameters = { + {"projection", CE::Rendering::Parameters::Type::Mat4, true, CE::Rendering::Parameters::Semantic::Projection} + }; + definition.program_sources = {root / "graphics/shaders/tgui.vert", root / "graphics/shaders/tgui-solid.frag"}; + const CE::Rendering::OpenGL::PipelineBindings solid{{{"projection", "projectionMatrix"}}}; + CE::UI::RmlUi::Materials result; + result.solid = native->build_material({native->build_pipeline(definition, solid), {}}); + definition.program_sources[1] = root / "graphics/shaders/tgui-textured.frag"; + definition.parameters.push_back({"image", CE::Rendering::Parameters::Type::Sampler2D}); + const CE::Rendering::OpenGL::PipelineBindings textured{{{"projection", "projectionMatrix"}, {"image", "mytexture"}}}; + result.textured = native->build_material({native->build_pipeline(std::move(definition), textured), {}}); + return result; +} ``` -Put that helper in your game's `.cpp` file before its method definitions. It -uses the selected OpenGL resource provider to create the solid/textured colored -triangle pipelines with compatible blending and sampler semantics. A different -renderer needs its own compatible builder; the session itself has no OpenGL -dependency. +RmlUi supplies premultiplied pixels and vertex colors. These shaders multiply +image RGBA by vertex RGBA, and the material blends with `PremultipliedAlpha`. +Using the TGUI helper's straight alpha state would violate this contract. +Another renderer needs an equivalent builder on its graphics owner. + +## Retain application state and an event listener -## Retain the integration state +Define this small listener before the game class in its header. It records an +intent for the update hook to consume: + +```cpp +class ApplyListener final : public Rml::EventListener { + bool& requested_; + +public: + explicit ApplyListener(bool& requested) : requested_(requested) {} + void ProcessEvent(Rml::Event&) override { requested_ = true; } +}; +``` -Add these game members. Declare native widget handles after the session so -ordinary member destruction releases the external handles first: +Add these game members, alongside First Game's `engine_` and `asset_root_`: ```cpp CE::Runtime::PlatformDispatcher::Submission ui_platform_; -std::optional ui_uploader_; -CE::UI::TGUI::Materials ui_materials_; -CE::UI::TGUI::Scene ui_scene_; -std::future ui_pending_; -std::unique_ptr ui_; -tgui::EditBox::Ptr field_; +std::optional ui_uploader_; +CE::UI::RmlUi::Materials ui_materials_; +CE::UI::RmlUi::Scene ui_scene_; +std::future ui_pending_; +std::unique_ptr ui_; +Rml::ElementDocument* document_ = nullptr; +Rml::Element* apply_button_ = nullptr; +Rml::ElementFormControlInput* field_ = nullptr; +std::string player_name_; bool apply_requested_ = false; bool ignore_quit_until_release_ = false; +std::unique_ptr apply_listener_; ``` -During `init()` on platform, after graphics is initialized: +The session owns the document and elements. Their pointers are borrowed aliases; +the application owns the listener and detaches it before closing the session. +Keep native objects after the session in member declaration order. + +During platform-owned `init()`, after graphics initialization: ```cpp ui_platform_ = engine_.platform_dispatcher().submission(); ui_uploader_.emplace(engine_.resources()); -ui_materials_ = materials(engine_.resources(), asset_root_); +ui_materials_ = make_rmlui_materials(engine_.resources(), asset_root_); ``` -Do not create native toolkit objects there when concurrent mode will update UI -on simulation. The uploader is constructed on its provider's owner; its saved -handle can then be used to submit owned recordings from the UI owner. +The uploader belongs to the resource provider's platform owner. Upload work +uses owned recordings; live document operations stay on the UI owner. -## Create native widgets on the first update +## Load the document on the first update -After the game's quit handling, initialize the session from the simulation hook: +Create the session on the owner that will update its UI. In concurrent mode +that is simulation, so run this block in `update()` after quit handling: ```cpp if (!ui_) { - ui_ = std::make_unique(engine_.input(), 2); + ui_ = std::make_unique(engine_.input(), 7); + if (!ui_->load_font(asset_root_ / "fonts/DejaVuSans.ttf", "tutorial")) + throw std::runtime_error("Cannot load the UI font"); ui_->set_view(tick.logical_size, tick.framebuffer_size); - - auto button = tgui::Button::create("Apply"); - button->setSize({120, 32}); - button->setPosition({16, 16}); - button->onPress([this] { apply_requested_ = true; }); - ui_->gui().add(button); - - field_ = tgui::EditBox::create(); - field_->setSize({240, 32}); - field_->setPosition({16, 60}); - field_->setDefaultText("Type a name"); - ui_->gui().add(field_); + const auto source = (asset_root_ / "graphics/ui/tutorial.rml").u8string(); + document_ = ui_->context().LoadDocument(Rml::String(source.begin(), source.end())); + if (!document_) + throw std::runtime_error("Cannot load the UI document"); + apply_button_ = document_->GetElementById("apply"); + field_ = dynamic_cast(document_->GetElementById("name")); + if (!apply_button_ || !field_) + throw std::runtime_error("The UI document requires an Apply button and a text input"); + apply_listener_ = std::make_unique(apply_requested_); + apply_button_->AddEventListener("click", apply_listener_.get()); + document_->Show(Rml::ModalFlag::None, Rml::FocusFlag::None); ui_->request_keyboard_focus(); - field_->setFocused(true); -} else { + if (!field_->Focus()) { + ui_->release_keyboard_focus(); + throw std::runtime_error("Cannot focus the UI field"); + } +} else ui_->set_view(tick.logical_size, tick.framebuffer_size); -} ``` -Target `2` is a chosen nonzero routing identity. Reserve distinct IDs for other -sessions in the application. Session construction requests Events capture; -keyboard focus requests Text capture. The widget's own focused state and the -session's routed keyboard ownership are related but separate. +Target `7` is a chosen nonzero routing identity; reserve a distinct target for +every other session. RmlUi permits one session owning Core's process globals. +Add more documents to that session when extending the interface. -The session and all native toolkit objects remain on this UI owner. The -`engine_.input()` getter borrows the adapter; it does not poll or mutate bindings. -Keep that adapter alive through the session's final closure. +Font registration retains owned bytes through Core shutdown. Showing a document, +focusing its field and acquiring routed keyboard focus are distinct operations. +Routing requests affect later platform polls; already captured text keeps its +original target and epoch. ## Feed records and control focus -At the beginning of `update()`, guard the existing quit check before creating -widgets or processing records: +At the beginning of `update()`, replace First Game's quit check with: ```cpp if (tick.input.button(quit_).pressed() && !ignore_quit_until_release_) { @@ -137,46 +281,53 @@ ui_->handle_input(tick.input.records(), true, [&](const CE::Input::Events::Recor } else if (button->key == CE::Input::KeyboardKey::F2 && button->phase == CE::Input::Events::ButtonPhase::Press) { ui_->request_keyboard_focus(); - field_->setFocused(true); + if (!field_->Focus(true)) { + ui_->release_keyboard_focus(); + throw std::runtime_error("Cannot focus the UI field"); + } } } return true; }); ui_->update_time(tick.delta_seconds); -if (std::exchange(apply_requested_, false)) - field_->setText("Applied"); +if (std::exchange(apply_requested_, false)) { + player_name_ = field_->GetValue(); + field_->SetValue("Applied"); +} ``` -The `true` values explicitly give this visible interface pointer delivery for -the task. A real overlay must decide hit testing and gameplay pointer ownership; -exclusive keyboard focus does not automatically capture/gate mouse gameplay. -Feed the whole batch once, including releases needed by modifier tracking. - -Escape releases routed focus and F2 requests it again. Requests take effect on -later platform polls; the session retains its entry epoch for the current batch -rather than relabeling records. While exclusive focus is active, First Game's -keyboard quit/movement actions are gated. When focus ends, a still-held key can -resume as a semantic press. Keep the game's quit check before this record handling -and require `!ignore_quit_until_release_` in addition to the quit action's -`pressed()` result. The releasing Escape stays suppressed through its physical -release batch; a later Escape press can reach the ordinary quit action. The -maintained Controls class exposes this decision through `permits_quit()`. +The listener runs on this UI owner. Apply copies the edited value into +game-owned `player_name_`; changing the field to `Applied` confirms the action. +Application text should go through native text/value setters. `SetInnerRML()` +parses markup, so escape user text before inserting it as RML. + +The `true` values explicitly select pointer delivery for this visible interface. +A game must choose hit testing and gameplay pointer ownership separately; +keyboard focus does not gate mouse gameplay. Feed the whole batch once, +including releases used for modifier tracking. + +Escape releases editing and F2 restores it. While exclusive focus is active, +First Game's keyboard movement and quit actions are gated. Ending focus can +resume a held key as a semantic press, so the quit check stays before record +handling and its guard remains set through the releasing Escape's physical key +release. A later Escape press can quit. The session keeps the entry epoch for +the entire batch; the callback does not relabel existing records. ## Upload without queue growth and publish At the end of update: ```cpp -static_cast(CE::UI::TGUI::adopt_scene(ui_pending_, ui_scene_)); +static_cast(CE::UI::RmlUi::adopt_scene(ui_pending_, ui_scene_)); if (!ui_pending_.valid()) ui_pending_ = ui_uploader_->submit(ui_platform_, ui_->record(), ui_materials_); ``` Adoption does not wait on pending work. Keep the previous complete scene while -one replacement waits; only owned recording/material data crosses to graphics, -never live widgets. Ready upload errors propagate here for the game's selected -failure policy. The maintained demo catches them to retain/report its previous -scene; allowing propagation uses runtime's normal failure cleanup. +one replacement waits; only owned recording/material data crosses to graphics. +Ready upload errors propagate here through runtime's normal failure cleanup. +The maintained demo instead catches them to report the error and keep its last +complete scene. After your world/HUD passes in `prepare_render_frame()`: @@ -184,43 +335,46 @@ After your world/HUD passes in `prepare_render_frame()`: ui_scene_.write(frame); ``` -This writes retained packet values, not native toolkit operations. The first -scene may be empty until upload completes. Authored pass order puts the UI after -the world. Runtime publishes and renders it without accessing your live widget -tree. +This appends retained draw packets using the scene's screen projection. The +first scene can be empty until upload completes. Later document edits cannot +alter resources already retained by an earlier published frame. ## Close after UI quiescence -In `deinit()`, after simulation has joined and accepted platform dependencies -settled: +In `deinit()`, after simulation has joined and accepted dependencies have settled: ```cpp -field_.reset(); +if (apply_button_ && apply_listener_) + apply_button_->RemoveEventListener("click", apply_listener_.get()); +apply_listener_.reset(); +apply_button_ = nullptr; +field_ = nullptr; +document_ = nullptr; if (ui_) { ui_->close_after_quiescence(); ui_.reset(); } -ui_scene_ = CE::UI::TGUI::Scene{}; +ui_scene_ = CE::UI::RmlUi::Scene{}; ui_uploader_.reset(); -ui_materials_ = CE::UI::TGUI::Materials{}; +ui_materials_ = CE::UI::RmlUi::Materials{}; ``` -Keep the game's other partial-safe cleanup. Drop every application-held native -widget/font/texture reference before session closure; the GUI releases its own -hierarchy during shutdown. Do not race closure with input, callbacks or UI -mutation. Retained Cheryl scenes can outlive toolkit objects while their -graphics domain remains live. - -The field should accept committed text, arrows and Backspace while focused. -Clicking Apply should display `Applied`; Escape releases editing and F2 restores -it. Resizing should update layout/view dimensions, and both runtime modes should -preserve input and teardown boundaries. Clipboard, cursor and IME services remain -limited by reported capabilities, regardless of what widgets normally support -on another backend. - -Read [GUI construction](../../gui/index.md), -[TGUI contracts](../../modules/tgui.md), +Keep the game's other partial-safe cleanup. The listener must detach and die +while Core is still alive. Drop application-held document/element/resource +references before session closure; the session releases its documents. Retained +Cheryl scenes can outlive the toolkit while their graphics domain remains live. + +Expect a styled panel with a focused field. Type a name and click Apply; the +game value should receive that name and the field should show `Applied`. +Check arrows, Backspace, Escape releasing editing, F2 restoring it, and a later +Escape quitting gameplay. Resize the window and check both runtime modes. +Clipboard, cursor and IME services retain the reported capability limits. + +The [maintained RmlUi demo](../../../projects/apps/demo/src/rmlui-demo.cpp) +demonstrates native document loading, listeners, hit testing and recoverable +uploads; it uses [its own RML document](../../../assets/graphics/ui/demo.rml). +The `tutorial-game` executable uses TGUI controls and does not implement this +RmlUi example. Read [RmlUi contracts](../../modules/rmlui.md), +[GUI construction](../../gui/index.md) and [focus routing](../../runtime/input-state-model.md#focus-routing-and-editing-controls) -and [the maintained Controls implementation](../../../projects/apps/tutorial-game/src/controls.cpp) -for the complete maintained task. [The larger demo](../../../projects/apps/demo/src/tgui-demo.cpp) -also illustrates layout, pointer hit testing, coexistence and recoverable uploads. +for the underlying boundaries. diff --git a/docs/getting-started/how-to/index.md b/docs/getting-started/how-to/index.md index 307053bd..dab733db 100644 --- a/docs/getting-started/how-to/index.md +++ b/docs/getting-started/how-to/index.md @@ -5,9 +5,11 @@ and [First Game](../first-game.md). Each task identifies its loading, update, frame-preparation and cleanup boundaries, then links the detailed contract. The [maintained tutorial game](../../../projects/apps/tutorial-game/README.md) -combines all six tasks with Player, Enemy and HUD classes and a small explicit -asset set. Its compile target is `tutorial-game`; native observation of rendering, -fonts and focus remains a separate acceptance step. +demonstrates input, sprites, animation, camera movement and direct text updates +with a TGUI controls panel. The interactive GUI task below teaches a separate +RmlUi document workflow; [Create a HUD](../create-a/hud.md) covers TGUI widgets. +Native observation of rendering, fonts and focus remains a separate acceptance +step from compiling an example. ```{toctree} :maxdepth: 1 @@ -17,7 +19,7 @@ Load and draw a sprite Play sprite animation Display and update text Position objects with a camera -Create an interactive GUI +Create an interactive GUI with RmlUi ``` For a complete application-owned class, continue with diff --git a/docs/getting-started/how-to/update-text.md b/docs/getting-started/how-to/update-text.md index 1ac3986f..c89a91a8 100644 --- a/docs/getting-started/how-to/update-text.md +++ b/docs/getting-started/how-to/update-text.md @@ -1,81 +1,111 @@ # Display and update text -Text crosses three stages: CPU layout/preparation, graphics upload, then CPU -frame submission. Start with [First Game](../first-game.md), which already has -the matching `first-game:text` material. This task changes a label without doing -graphics work on concurrent simulation or mutating resources retained by an -earlier frame. +Extend [First Game's text walkthrough](../first-game.md#put-text-on-screen) +with a score that changes during play. Text crosses three stages: CPU +layout/preparation, graphics upload, then CPU frame submission. This task uses +the existing `first-game:text` material and keeps an earlier frame's resources +valid while the next label uploads. + +## Retain the replacement state + +Keep First Game's `score_` generation and `text_` material. Add these members to +the game: + +```cpp +std::optional fonts_; +CE::Runtime::PlatformDispatcher::Submission text_platform_; +std::future pending_score_; +int requested_score_ = 0; +int displayed_score_ = 0; +int submitted_score_ = 0; +``` + +Include ``, ``, +``, ``, ``, ``, `` and `` +alongside First Game's text/submission headers. The optional font collection +allows loading during `init()` and cleanup after partial initialization. + +Game rules own the score's meaning. These three integers distinguish the latest +requested value, the value currently shown, and the value in the pending upload. ## Load fonts and an initial generation -Include the font collection, layout, Unicode text and submission headers used -by [the maintained example](../../../projects/apps/first-game/src/first-game.cpp). -During platform-owned initialization: +Replace First Game's initial score block with this platform-owned `init()` code: ```cpp +text_platform_ = engine_.platform_dispatcher().submission(); CE::Text::FontSelection selection; selection.preferred = {"DejaVu Sans"}; selection.asset_directories = {asset_root_}; selection.automatic_system_fonts = false; -auto fonts = CE::Text::FontCollection::load(selection); -auto layout = CE::Text::layout_text(fonts, "Score: 0", {.pixel_height = 24}); +fonts_.emplace(CE::Text::FontCollection::load(selection)); +auto layout = CE::Text::layout_text(*fonts_, "Score: 0", {.pixel_height = 24}); auto prepared = CE::Text::Rendering::prepare(std::move(layout)); -auto initial = CE::Text::Rendering::upload(std::move(prepared), engine_.resources()); +score_.emplace(CE::Text::Rendering::upload(std::move(prepared), engine_.resources())); ``` -Retain `fonts` for replacements and move `initial` into an optional -`CE::Text::Rendering::Generation` member. Selection snapshots font bytes; missing -preferred fonts can fall back according to the collection contract. Explicit -application font assets give repeatable appearance across machines. +Font selection snapshots bytes; layout/prepared values retain those snapshots. +Missing preferred fonts can fall back according to the collection contract. +Explicit application font assets give repeatable appearance across machines. +The saved dispatcher endpoint submits work without capturing a borrowed context. -## Prepare a replacement only when content changes +## Request a changed value -Keep the requested score, the displayed/submitted score and at most one -`std::future pending_`. Include ``, -`` and ``. During update, when no request is pending and content -differs from the displayed value: +Assign `requested_score_` when game rules change the score. For a simple +extension to First Game, add this after its existing gameplay updates so Space +also increments the score: ```cpp -auto replacement = CE::Text::Rendering::prepare( - CE::Text::layout_text(fonts_, std::format("Score: {}", requested_score_), {.pixel_height = 24})); -pending_ = engine_.platform_dispatcher().submit( - [prepared = std::move(replacement)](CE::Runtime::Context& owner) mutable { - return CE::Text::Rendering::upload(std::move(prepared), owner.resources()); - }); -submitted_score_ = requested_score_; +if (tick.input.button(next_cell_).pressed() && requested_score_ < std::numeric_limits::max()) + ++requested_score_; ``` -Only owned prepared data crosses to graphics; the callback does not capture the -HUD or a borrowed string. Layout/preparation is CPU-only. For larger text, run -that CPU stage through a tracked worker group and retain the same upload -boundary. The simple label needs no unbounded queue of intermediate scores. +Use an appropriate bounded score type/policy for the actual game. -## Adopt without blocking simulation +## Adopt and prepare without blocking simulation -Before considering another submission in update: +After handling score changes, run this block during each `update()`: ```cpp -if (pending_.valid() && pending_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - auto replacement = pending_.get(); +if (pending_score_.valid() && pending_score_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + auto replacement = pending_score_.get(); score_.emplace(std::move(replacement)); displayed_score_ = submitted_score_; } +if (!pending_score_.valid() && requested_score_ != displayed_score_) { + auto prepared = CE::Text::Rendering::prepare( + CE::Text::layout_text(*fonts_, std::format("Score: {}", requested_score_), {.pixel_height = 24})); + pending_score_ = text_platform_.submit( + [prepared = std::move(prepared)](CE::Runtime::Context& owner) mutable { + return CE::Text::Rendering::upload(std::move(prepared), owner.resources()); + }); + submitted_score_ = requested_score_; +} ``` Keep drawing the previous complete generation while waiting. A ready exception -propagates before replacing `score_`; decide whether the application reports it -and retries or allows runtime's normal failure cleanup. If the requested value -changed while upload waited, submit the latest value after adopting this one. +propagates before replacing `score_`; this example uses runtime's normal failure +cleanup. An application that catches it must choose its reporting/retry policy. Do not call `get()` on a still-pending platform dependency from simulation. -## Publish and release +Only owned prepared data crosses to graphics. The callback captures neither the +game nor a borrowed string. Small-label layout/preparation runs on simulation; +larger content can use a tracked worker group with the same upload boundary. -In frame preparation, use a compatible text material and image key: +One pending request bounds queue growth. Further changes coalesce into the next +replacement, and unchanged values submit no work. For example, if `10` uploads +while the requested value advances through `11` to `12`, adoption displays `10` +and the next request prepares `12`. + +## Publish in screen coordinates and release + +Keep First Game's compatible text material and image key when submitting: ```cpp CE::Rendering::DrawStyle2D style; style.material = text_; -style.model_matrix = glm::translate(glm::mat4(1.0f), glm::vec3(24.0f, 280.0f, 0.0f)); +style.model_matrix = + glm::translate(glm::mat4(1.0f), glm::vec3(24.0f, static_cast(camera_.framebuffer_size().height) - 48.0f, 0.0f)); pass.add(CE::Rendering::Submission::resolve_text(*score_, style, context)); ``` @@ -84,12 +114,26 @@ Here `context` is the pass's `Context2D` with image parameter `"image"`, as in changing the source string alone cannot change an uploaded label. A new generation leaves old frame packets valid. -During cleanup reset the game-owned generation after runtime has settled -accepted work and recycled frames. Keep provider/material dependencies alive -until then. The score should update after replacement completes, remain visible -while waiting and preserve correct placement across runtime modes. +For a scrolling world, use +[First Game's separate screen pass](../first-game.md#keep-text-in-screen-coordinates) +instead of sharing the world camera. + +During `deinit()`, after runtime has settled accepted work and recycled frames: + +```cpp +score_.reset(); +fonts_.reset(); +``` + +Keep provider/material dependencies alive through the game's other cleanup. +The score should start at zero, update after replacement completes, remain +visible while waiting and preserve screen placement during resize and world +movement. Check both runtime modes; repeated unchanged updates should submit no +replacement uploads. -[Create a HUD](../create-a/hud.md) supplies the complete class and coalescing -state. Read [Unicode layout](../../assets/text-layout.md), +This is the direct text rendering path. [Create a HUD with TGUI](../create-a/hud.md) +builds score and health widgets, while [the RmlUi GUI guide](create-gui.md) +covers documents and interactive editing. Read +[Unicode layout](../../assets/text-layout.md), [encoding/source indices](../../assets/text-encoding.md) and [resource publication](../../resources/resource-residency.md) for detailed limits. diff --git a/docs/getting-started/project-setup.md b/docs/getting-started/project-setup.md index 30fb7865..bdbd5b3e 100644 --- a/docs/getting-started/project-setup.md +++ b/docs/getting-started/project-setup.md @@ -287,7 +287,7 @@ others, choose its build, install or packaging operation: | Need | Operation | Result | | --- | --- | --- | | Run the introductory walkthrough | Select `CHERYL_BUILD_FIRST_GAME=ON`; build `first-game`. | The small sprite/text game and its linked libraries. | -| Run Player/Enemy/HUD and How-to examples | Select `CHERYL_BUILD_TUTORIAL_GAME=ON` with Startup/OpenGL/native input/TGUI; build `tutorial-game`. | One maintained teaching game with explicit small assets. | +| Run the Player/Enemy and direct text example | Select `CHERYL_BUILD_TUTORIAL_GAME=ON` with Startup/OpenGL/native input/TGUI; build `tutorial-game`. | Includes TGUI controls; the widget HUD and RmlUi guides are separate extensions. | | Build a library directly | Build `cengine`, `module_opengl` or another concrete library target. | The selected library and dependencies in the build tree. | | Build selected SDK libraries | Select `CHERYL_BUILD_SDK=ON`; build custom target `cheryl-sdk`, which builds the selected libraries together. | Compiled installable libraries; headers have not yet been collected. | | Collect libraries and headers | After that build, use `cmake --install` with `--component CherylSDK` and a destination `--prefix`. | One SDK directory with headers, libraries, CMake package files and licenses. | diff --git a/docs/gui/index.md b/docs/gui/index.md index 2f79911a..7c8886da 100644 --- a/docs/gui/index.md +++ b/docs/gui/index.md @@ -10,8 +10,8 @@ widget construction, input routing, focus and a publication path. | Integration | Application authors | Guide | | --- | --- | --- | -| TGUI | Native C++ widgets and layouts through the session's `gui()` object. | [TGUI owner guide](../modules/tgui.md) | -| RmlUi | Native contexts, RML documents and RCSS styles through a document session. | [Create an editable document](../modules/rmlui.md#create-an-editable-document) | +| TGUI | Native C++ widgets and layouts through the session's `gui()` object. | [Create a HUD](../getting-started/create-a/hud.md); [TGUI contracts](../modules/tgui.md) | +| RmlUi | Native contexts, RML documents and RCSS styles through a document session. | [Create an interactive GUI](../getting-started/how-to/create-gui.md); [RmlUi contracts](../modules/rmlui.md) | Neither integration provides a common widget API. Select one according to the application's authoring needs. Both convert ordered Engine input and publish @@ -89,8 +89,10 @@ callbacks or UI operations can still run. ## Continue -Use [the GUI task guide](../getting-started/how-to/create-gui.md) for focused -integration, [GUI system relationships](../engine-systems/gui.md) for ownership, +Use [the TGUI HUD tutorial](../getting-started/create-a/hud.md) for a widget +class, [the RmlUi GUI task](../getting-started/how-to/create-gui.md) for document +authoring and editing, [GUI system relationships](../engine-systems/gui.md) for +ownership, and [input focus contracts](../runtime/input-state-model.md#focus-routing-and-editing-controls) for routing detail. Clipboard, cursor, composition and other services depend on reported integration capabilities; a text field does not imply a complete IME diff --git a/projects/apps/tutorial-game/README.md b/projects/apps/tutorial-game/README.md index ee95e3aa..c4749f13 100644 --- a/projects/apps/tutorial-game/README.md +++ b/projects/apps/tutorial-game/README.md @@ -1,11 +1,18 @@ # Tutorial game -This maintained example combines the [Player, Enemy and HUD tutorials](../../../docs/getting-started/create-a/index.md) -and [six focused tasks](../../../docs/getting-started/how-to/index.md). It consumes -the standard Startup module, Native GLFW, OpenGL and TGUI. The two-cell actor and -base image/text shaders are copied from First Game; the colored UI shaders come -from the main demo. Its small asset set is listed explicitly in CMake and needs -no optional external art download. +This maintained example combines the [Player and Enemy classes](../../../docs/getting-started/create-a/index.md) +with input, sprite animation, camera movement, a direct text score and TGUI +controls. The [focused tasks](../../../docs/getting-started/how-to/index.md) +explain its input, sprite, camera and text operations. It consumes the standard +Startup module, Native GLFW, OpenGL and TGUI. The two-cell actor and base +image/text shaders are copied from First Game; the colored UI shaders come from +the main demo. Its small asset set is listed explicitly in CMake and needs no +optional external art download. + +The [TGUI widget HUD tutorial](../../../docs/getting-started/create-a/hud.md) and +[RmlUi GUI guide](../../../docs/getting-started/how-to/create-gui.md) describe +separate extensions to First Game. This executable uses its direct text `Hud` and +TGUI `Controls` classes; it does not implement those two interfaces. The game starts with an editable field focused. Escape releases it; a later Escape press quits. F2 returns focus to editing. With editor focus released, diff --git a/projects/modules/ui/rmlui/README.md b/projects/modules/ui/rmlui/README.md index 9ff681d3..c5582629 100644 --- a/projects/modules/ui/rmlui/README.md +++ b/projects/modules/ui/rmlui/README.md @@ -18,167 +18,16 @@ target_link_libraries(game PRIVATE Cheryl::UI::RmlUi) ## Create an editable document -Start from the game lifecycle in First Game. This example adds an editable RML -field, with the same owner boundaries used by the maintained -[demo implementation](../../../apps/demo/src/rmlui-demo.cpp). It uses OpenGL -materials, the shaders `tgui.vert`, `tgui-solid.frag` and `tgui-textured.frag` under -`graphics/shaders/`, and an explicit TrueType/OpenType font file. Copy those shader -files from the checkout's assets when extending First Game's smaller asset root. -The indexed loader does not construct these UI materials. - -Include ``, -``, `` and -``, plus ``, ``, ``, -``, `` and ``. Keep the following members in your game, alongside -its context reference `engine_`, asset root `root_` and font path `font_`: - -```cpp -std::optional uploader_; -CE::UI::RmlUi::Materials materials_; -std::unique_ptr ui_; -Rml::ElementDocument* document_ = nullptr; -CE::UI::RmlUi::Scene scene_; -std::future pending_; -``` - -### Prepare materials during initialization - -Define this application helper using the maintained demo's material construction. -Its shaders multiply vertex/image RGBA; RmlUi requires premultiplied blending: - -```cpp -CE::UI::RmlUi::Materials make_rmlui_materials( - CE::Rendering::OpenGL::ResourceProvider& native, const std::filesystem::path& root -) { - CE::Rendering::Pipelines::Definition definition; - definition.vertex_layout = CE::Rendering::Geometry::Layout2D::Position3UV2Color4; - definition.topology = CE::Rendering::Geometry::Topology::Triangles; - definition.state = {CE::Rendering::Pipelines::Blend::PremultipliedAlpha, - CE::Rendering::Pipelines::Depth::Disabled, false, - CE::Rendering::Pipelines::Cull::None}; - definition.parameters = {{"projection", CE::Rendering::Parameters::Type::Mat4, true, - CE::Rendering::Parameters::Semantic::Projection}}; - definition.program_sources = {root / "graphics/shaders/tgui.vert", - root / "graphics/shaders/tgui-solid.frag"}; - const CE::Rendering::OpenGL::PipelineBindings solid{{{"projection", "projectionMatrix"}}}; - CE::UI::RmlUi::Materials result; - result.solid = native.build_material({native.build_pipeline(definition, solid), {}}); - definition.program_sources[1] = root / "graphics/shaders/tgui-textured.frag"; - definition.parameters.push_back({"image", CE::Rendering::Parameters::Type::Sampler2D}); - const CE::Rendering::OpenGL::PipelineBindings textured{ - {{"projection", "projectionMatrix"}, {"image", "mytexture"}} - }; - result.textured = native.build_material({native.build_pipeline(std::move(definition), textured), {}}); - return result; -} -``` - -On platform during `init()`, after renderer initialization: - -```cpp -auto* native = dynamic_cast(&engine_.resources()); -if (!native) - throw std::runtime_error("This material builder requires OpenGL"); -materials_ = make_rmlui_materials(*native, root_); -uploader_.emplace(engine_.resources()); -``` - -Another graphics backend supplies compatible materials through its own builder. -The [rendering guide](../../../../docs/rendering/pipelines-and-materials.md#manual-material-construction) -defines parameter contracts and native bindings. - -### Create the session on the first update - -A concurrent game's UI owner starts with `update()`, after platform-owned `init()`. -Run this block once there. Choose a distinct nonzero routing target for each UI; -this example uses `7`: - -```cpp -if (!ui_) { - ui_ = std::make_unique(engine_.input(), 7); - if (!ui_->load_font(font_, "tutorial")) - throw std::runtime_error("Cannot load the UI font"); - ui_->set_view(tick.logical_size, tick.framebuffer_size); - document_ = ui_->context().LoadDocumentFromMemory(R"( -
Player name
- -
- )"); - if (!document_) - throw std::runtime_error("Cannot load the UI document"); - document_->Show(Rml::ModalFlag::None, Rml::FocusFlag::None); - auto* field = document_->GetElementById("name"); - if (!field || !field->Focus()) - throw std::runtime_error("Cannot focus the UI field"); - ui_->request_keyboard_focus(); -} -``` - -The document owns its elements; `document_` borrows the session's context. Font -bytes remain valid through Core shutdown. Routing/capture affect later polls, so -text already captured under another target does not move to the new field. - -### Process input and publish a scene - -After creation, each `update()` supplies copied sizes, simulation time and the -complete tick batch once: - -```cpp -ui_->set_view(tick.logical_size, tick.framebuffer_size); -ui_->update_time(tick.delta_seconds); -ui_->handle_input(tick.input.records(), true); -static_cast(CE::UI::RmlUi::adopt_scene(pending_, scene_)); -if (!pending_.valid()) - pending_ = uploader_->submit(engine_.platform_dispatcher().submission(), ui_->record(), materials_); -``` - -`true` explicitly assigns pointer input to this interface. Adopt only a ready, -complete upload; the first scene can be empty while the request is pending. -An upload/cancellation error propagates without replacing the previous scene. -The demo catches that error to report it and continue; propagation here selects -normal runtime failure cleanup. Keeping one pending replacement bounds the queue. - -During `prepare_render_frame()`, after world/HUD passes: - -```cpp -scene_.write(frame); -``` - -The frame contains retained packets, never live document operations. To end -editing on the UI owner, call `ui_->release_keyboard_focus()`; native window close -remains available while the field owns keyboard gameplay input. Application focus -shortcuts use the session's ordered `before_record` overload, as in the demo. - -### Close after UI quiescence - -During `deinit()`, after simulation joins and accepted dependencies settle: - -```cpp -document_ = nullptr; -if (ui_) { - ui_->close_after_quiescence(); - ui_.reset(); -} -scene_ = CE::UI::RmlUi::Scene{}; -uploader_.reset(); -materials_ = CE::UI::RmlUi::Materials{}; -``` - -Detach and release any listeners or toolkit resources you add before closing the -session. Keep the rest of game cleanup tolerant of partial initialization. -[Ownership and Cleanup](../../../../docs/architecture/ownership-cleanup.md) -connects these steps to the runtime's shutdown dependencies. - -The field should appear below the label, accept committed text and editing keys, -retain its placement during resize, and close cleanly in both runtime modes. This -minimal interface uses supported rectangular drawing. Clipboard, cursor services, -IME, transforms and advanced effects retain the capability limits below. +[Create an interactive GUI with RmlUi](../../../../docs/getting-started/how-to/create-gui.md) +provides the full First Game extension: module selection, font/shader assets, +RML and RCSS files, compatible premultiplied materials, native event listeners, +focus routing, retained scene publication and partial-safe cleanup. + +The [maintained RmlUi demo](../../../apps/demo/src/rmlui-demo.cpp) also +demonstrates document loading, hit testing and recoverable upload errors. +For a C++ widget HUD using the other integration, follow +[Create a HUD with TGUI](../../../../docs/getting-started/create-a/hud.md). +The contracts below describe RmlUi session ownership and supported behavior. ## Native documents and input diff --git a/projects/modules/ui/tgui/README.md b/projects/modules/ui/tgui/README.md index 0a360dda..fb871bb1 100644 --- a/projects/modules/ui/tgui/README.md +++ b/projects/modules/ui/tgui/README.md @@ -43,9 +43,11 @@ before selecting the module. ## Use it in a game -[Create an interactive GUI](../../../../docs/getting-started/how-to/create-gui.md) -provides the complete button/edit-field workflow, including materials and focus -controls. Its lifecycle is: +[Create a HUD with TGUI](../../../../docs/getting-started/create-a/hud.md) +provides the complete score-label/health-bar workflow, including materials, +layout and game-state updates. The +[maintained TGUI demo](../../../apps/demo/src/tgui-demo.cpp) also supplies +interactive buttons, editing and application focus controls. The lifecycle is: 1. During platform-owned `init()`, construct application materials and a scene uploader. 2. On the first simulation update, create a session and native widgets. From f50f633e6f00e0f6e8503940f54db4ccad64ae55 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 17:20:58 -0700 Subject: [PATCH 46/98] Adds clipboard copy controls to documentation code blocks Registers shared JavaScript and CSS that place an accessible copy icon at the top right of highlighted code, included excerpts and plain literal blocks. Preserves source whitespace, excludes line numbers and reports copy success or failure. Supports light and dark themes, keyboard focus and local documentation without the Clipboard API. Restores focus and text selection after the temporary-field fallback and hides controls when printing. Checks JavaScript syntax, registered assets and the existing configuration tests. Exercises browser fixtures with mocked clipboard writes, failure and fallback paths, exact copied text, numbered blocks, retry/reset behavior and desktop/mobile layouts. Full documentation generation was not run. --- docs/generation/conf.py | 4 +- docs/generation/static/copy-code.css | 79 +++++++++++++++++ docs/generation/static/copy-code.js | 124 +++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 docs/generation/static/copy-code.css create mode 100644 docs/generation/static/copy-code.js diff --git a/docs/generation/conf.py b/docs/generation/conf.py index ce4d9c7e..1db26d17 100644 --- a/docs/generation/conf.py +++ b/docs/generation/conf.py @@ -100,8 +100,8 @@ html_theme = "sphinx_rtd_theme" templates_path = ["templates"] html_static_path = ["static"] -html_css_files = ["hierarchy.css", "typography.css", "layout.css", "theme.css"] -html_js_files = ["appearance.js"] +html_css_files = ["hierarchy.css", "typography.css", "layout.css", "theme.css", "copy-code.css"] +html_js_files = ["appearance.js", "copy-code.js"] html_title = "Cheryl Engine documentation" html_theme_options = {"navigation_depth": 4, "collapse_navigation": False} html_show_sourcelink = True diff --git a/docs/generation/static/copy-code.css b/docs/generation/static/copy-code.css new file mode 100644 index 00000000..8db9a4e0 --- /dev/null +++ b/docs/generation/static/copy-code.css @@ -0,0 +1,79 @@ +.cheryl-copy-feedback { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +@media screen { + .rst-content .cheryl-code-block { + position: relative; + } + + .rst-content .cheryl-copy-source { + padding-right: 3.25rem; + } + + .rst-content .cheryl-copy-code { + position: absolute; + top: 0.375rem; + right: 0.375rem; + display: flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + padding: 0; + border: 1px solid var(--cheryl-border, #c9cfd6); + border-radius: 0.2rem; + background: var(--cheryl-surface, #fff); + color: var(--cheryl-text, #3f4e5f); + cursor: pointer; + } + + .rst-content .cheryl-copy-code:hover { + border-color: var(--cheryl-link, #216aa0); + color: var(--cheryl-link, #216aa0); + } + + .rst-content .cheryl-copy-code:focus-visible { + outline: 2px solid var(--cheryl-focus, #216aa0); + outline-offset: 2px; + } + + .rst-content .cheryl-copy-code svg { + width: 1rem; + height: 1rem; + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; + } + + .rst-content .cheryl-copy-code[data-state="copied"] { + color: #18723e; + } + + .rst-content .cheryl-copy-code[data-state="failed"] { + color: #a12622; + } + + html[data-theme="dark"] .cheryl-copy-code[data-state="copied"] { + color: #a9dca1; + } + + html[data-theme="dark"] .cheryl-copy-code[data-state="failed"] { + color: #ffaaaa; + } +} + +@media print { + .cheryl-copy-code { + display: none; + } +} diff --git a/docs/generation/static/copy-code.js b/docs/generation/static/copy-code.js new file mode 100644 index 00000000..9efba2dd --- /dev/null +++ b/docs/generation/static/copy-code.js @@ -0,0 +1,124 @@ +/* Add copy controls without changing the text inside code blocks. */ +(() => { + "use strict"; + + const icons = { + copy: "M9 9h12v12H9z M15 9V3H3v12h6", + copied: "m5 12 4 4L19 6", + failed: "m6 6 12 12M18 6 6 18", + }; + + async function copyText(text) { + if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") { + await navigator.clipboard.writeText(text); + return; + } + + // Locally opened documentation may lack the Clipboard API. + const focused = document.activeElement; + const selection = window.getSelection(); + const ranges = []; + if (selection) { + for (let index = 0; index < selection.rangeCount; ++index) { + ranges.push(selection.getRangeAt(index).cloneRange()); + } + } + const field = document.createElement("textarea"); + field.value = text; + field.readOnly = true; + field.tabIndex = -1; + field.style.position = "fixed"; + field.style.left = "-10000px"; + document.body.append(field); + try { + field.focus({preventScroll: true}); + field.select(); + if (!document.execCommand("copy")) { + throw new Error("Clipboard copy failed"); + } + } finally { + field.remove(); + if (focused) { + focused.focus({preventScroll: true}); + } + if (selection) { + selection.removeAllRanges(); + ranges.forEach(range => selection.addRange(range)); + } + } + } + + function addCopyButtons() { + const blocks = document.querySelectorAll(".rst-content .highlight pre, .rst-content pre.literal-block"); + blocks.forEach(pre => { + if (pre.closest(".linenos, .linenodiv")) { + return; + } + let container = pre.closest(".highlight, .cheryl-code-block"); + if (container && container.classList.contains("cheryl-code-block")) { + return; + } + if (!container) { + container = document.createElement("div"); + pre.before(container); + container.append(pre); + } + container.classList.add("cheryl-code-block"); + pre.classList.add("cheryl-copy-source"); + + const button = document.createElement("button"); + button.type = "button"; + button.className = "cheryl-copy-code"; + const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + icon.setAttribute("viewBox", "0 0 24 24"); + icon.setAttribute("aria-hidden", "true"); + icon.setAttribute("focusable", "false"); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + icon.append(path); + button.append(icon); + + const feedback = document.createElement("span"); + feedback.className = "cheryl-copy-feedback"; + feedback.setAttribute("role", "status"); + feedback.setAttribute("aria-live", "polite"); + + function show(state, message) { + button.dataset.state = state; + button.title = message; + button.setAttribute("aria-label", message); + path.setAttribute("d", icons[state]); + feedback.textContent = state === "copy" ? "" : message; + } + + let copying = false; + let resetTimer; + show("copy", "Copy code to clipboard"); + button.addEventListener("click", async () => { + if (copying) { + return; + } + copying = true; + window.clearTimeout(resetTimer); + feedback.textContent = ""; + try { + const content = pre.cloneNode(true); + content.querySelectorAll(".linenos").forEach(number => number.remove()); + await copyText(content.textContent); + show("copied", "Copied to clipboard"); + } catch { + show("failed", "Copy failed. Select the code and copy it manually."); + } finally { + copying = false; + resetTimer = window.setTimeout(() => show("copy", "Copy code to clipboard"), 2500); + } + }); + container.append(button, feedback); + }); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", addCopyButtons, {once: true}); + } else { + addCopyButtons(); + } +})(); From 329b72cf129c4210a8bcd246f921cb62f5cdbad1 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sat, 17 Oct 2026 19:31:16 -0700 Subject: [PATCH 47/98] Merges fix/ui-default-materials into fixes-develop (squash) * Adds provider-owned default materials for UI adapters Extends the existing shader builder contract with built-in colored 2D materials. OpenGL owns embedded sources, validates them through its existing recipe path, and weakly reuses programs across alpha variants and materials within each provider. Adds lazy default upload and submission overloads to TGUI and RmlUi while preserving explicit material overrides and provider/thread checks. Migrates both demo views and tutorial controls, removing their native material helpers and tutorial shader copies. Covers default selection, caching, failures, overrides and queued runtime ownership with focused tests. Native OpenGL acceptance checks rendered color/alpha, cache sharing, retirement and rejected foreign/stopped-domain access. Focused Linux checks pass; Windows and relocated SDK acceptance have not been run for this change. * Updates UI guides for automatic rendering materials Removes application-owned OpenGL material helpers and UI shader-copy steps from the TGUI HUD and RmlUi GUI tutorials. Uses the default upload/submission overloads and initializes the RmlUi dispatcher handle explicitly. Documents provider-owned built-in profiles, lazy owner-thread construction, weak OpenGL cache ownership and unsupported-provider behavior. Shows custom UI material selection through loaded JSON handles while retaining the independent manual construction API and example. Synchronizes affected adapter, rendering, resource, demo and planning references. Checks local documentation links/includes and compiles both tutorial integrations as focused Linux syntax probes. * Fixes Arch SDK license notice discovery Resolve centrally installed SPDX texts from pacman's installed license metadata when a native library's package has no separate notice files. Preserve native-prefix and package-specific notice precedence, include declared alternatives and exceptions, and reject incomplete license sets. Covers libelf and glib2 shared license collection, wrapped expressions, exception texts, and missing or unsafe declarations with isolated fixtures. All 16 notice tests pass; notice collection also succeeds in a disposable directory for the configured college-ports SDK runtime inputs and staged system-library closure. Full SDK installation remains to be retried. --- .github/ci/native_notices.py | 41 ++++++- .github/ci/test_native_notices.py | 49 ++++++++ docs/assets/asset-loading.md | 4 +- docs/assets/asset-manifests.md | 4 +- docs/development/ui-adapters.md | 34 +++--- docs/engine-systems/gui.md | 5 +- docs/engine-systems/rendering.md | 1 + docs/getting-started/create-a/hud.md | 43 +++---- docs/getting-started/how-to/create-gui.md | 71 +++--------- docs/gui/index.md | 15 ++- .../develop-review-and-development-plan.md | 5 +- docs/rendering/pipelines-and-materials.md | 93 ++++++++++++++-- docs/resources/resource-residency.md | 4 + projects/apps/demo/README.md | 21 ++-- projects/apps/demo/src/demo-game.cpp | 2 +- projects/apps/demo/src/rmlui-demo.cpp | 30 +---- projects/apps/demo/src/tgui-demo.cpp | 39 +------ projects/apps/demo/src/tgui-demo.h | 5 +- projects/apps/tutorial-game/CMakeLists.txt | 3 - projects/apps/tutorial-game/README.md | 6 +- .../assets/graphics/shaders/tgui-solid.frag | 9 -- .../graphics/shaders/tgui-textured.frag | 12 -- .../assets/graphics/shaders/tgui.vert | 17 --- projects/apps/tutorial-game/src/controls.cpp | 40 +------ projects/apps/tutorial-game/src/controls.h | 4 +- projects/apps/tutorial-game/src/game.cpp | 2 +- .../cheryl/assets/definitions/pipeline.h | 5 + .../assets/resources/shader-asset-builder.h | 5 + .../assets/resources/shader-asset-builder.cpp | 10 ++ .../tests/include/testing/material-builder.h | 47 ++++++++ .../engine/tests/unit/src/shader-assets.cpp | 12 ++ projects/modules/graphics/opengl/README.md | 6 + .../backends/opengl/resource-provider.h | 8 ++ .../src/backends/opengl/builtin-materials.cpp | 94 ++++++++++++++++ .../tests/acceptance/src/native-opengl.cpp | 103 +++++++++++++++++ projects/modules/ui/rmlui/README.md | 43 ++++--- .../ui/rmlui/include/cheryl/ui/rmlui/scene.h | 5 + projects/modules/ui/rmlui/src/scene.cpp | 35 +++++- .../ui/rmlui/tests/unit/src/runtime.cpp | 26 ++++- .../modules/ui/rmlui/tests/unit/src/scene.cpp | 105 +++++++++++++++++- projects/modules/ui/tgui/README.md | 37 +++--- .../ui/tgui/include/cheryl/ui/tgui/scene.h | 5 + projects/modules/ui/tgui/src/scene.cpp | 35 +++++- .../ui/tgui/tests/unit/src/runtime.cpp | 26 ++++- .../modules/ui/tgui/tests/unit/src/scene.cpp | 103 ++++++++++++++++- 45 files changed, 933 insertions(+), 336 deletions(-) delete mode 100644 projects/apps/tutorial-game/assets/graphics/shaders/tgui-solid.frag delete mode 100644 projects/apps/tutorial-game/assets/graphics/shaders/tgui-textured.frag delete mode 100644 projects/apps/tutorial-game/assets/graphics/shaders/tgui.vert create mode 100644 projects/engine/src/assets/resources/shader-asset-builder.cpp create mode 100644 projects/engine/tests/include/testing/material-builder.h create mode 100644 projects/modules/graphics/opengl/src/backends/opengl/builtin-materials.cpp diff --git a/.github/ci/native_notices.py b/.github/ci/native_notices.py index 979f79cd..6ca9e629 100644 --- a/.github/ci/native_notices.py +++ b/.github/ci/native_notices.py @@ -2,6 +2,7 @@ import argparse import json +import os from pathlib import Path import re import subprocess @@ -85,7 +86,37 @@ def _references(source, system_root): return [path for path in references if path.is_file()] -def _notices(library, package, native_prefix, system_root): +def _arch_license_files(package, system_root, *, run): + try: + result = run(["pacman", "-Qi", "--", package], capture_output=True, text=True, check=False, + env={**os.environ, "LC_ALL": "C"}) + except FileNotFoundError: + return [] + if result.returncode: + return [] + match = re.search(r"^Licenses\s*:[ \t]*([^\n]*(?:\n[ \t]+[^\n]*)*)", result.stdout, re.MULTILINE) + if not match or not re.fullmatch(r"[A-Za-z0-9.+_()\s-]+", match[1]): + return [] + files = [] + exception = False + for name in re.findall(r"[A-Za-z0-9][A-Za-z0-9.+_-]*", match[1]): + if name in {"AND", "OR"}: + continue + if name == "WITH": + exception = True + continue + directory = system_root / "usr/share/licenses/spdx" + if exception: + directory /= "exceptions" + path = directory / (name + ".txt") + if not path.is_file(): + return [] + files.append(path) + exception = False + return files if not exception else [] + + +def _notices(library, package, native_prefix, system_root, *, run=subprocess.run): provider = _provider(library, package, native_prefix) if provider: return package or provider.name, provider, _override_files(provider), "native-prefix" @@ -95,6 +126,10 @@ def _notices(library, package, native_prefix, system_root): licenses = system_root / "usr/share/licenses" / name files = ([doc] if doc.is_file() else []) files.extend(sorted(path for path in licenses.rglob("*") if path.is_file()) if licenses.is_dir() else []) + # Arch keeps standard license texts centrally instead of duplicating + # them for each package. Include every declared alternative and exception. + if not files: + files = _arch_license_files(package, system_root, run=run) if files: return package, None, files, "installed-package" owner = package or "unknown package" @@ -122,7 +157,7 @@ def collect_notices(libraries, destination, *, native_prefix=None, system_root=P continue provider = _provider(library, None, native_prefix) package = provider.name if provider else package_owner(library, run=run) if query_packages else None - package, provider, seeds, origin = _notices(library, package, native_prefix, system_root) + package, provider, seeds, origin = _notices(library, package, native_prefix, system_root, run=run) directory = package.split(":")[0] pending = [(path, None) for path in seeds] seen, notice_names = set(), [] @@ -137,6 +172,8 @@ def collect_notices(libraries, destination, *, native_prefix=None, system_root=P relative = Path(directory) / path.relative_to(provider) elif path.name == "copyright" and path.parent.parent.name == "doc": relative = Path(directory) / "copyright" + elif path.is_relative_to(system_root / "usr/share/licenses/spdx"): + relative = Path(directory) / "licenses" / path.relative_to(system_root / "usr/share/licenses/spdx") else: relative = Path(directory) / "licenses" / path.relative_to(system_root / "usr/share/licenses" / directory) data = path.read_bytes() diff --git a/.github/ci/test_native_notices.py b/.github/ci/test_native_notices.py index e4168a22..49d2264a 100644 --- a/.github/ci/test_native_notices.py +++ b/.github/ci/test_native_notices.py @@ -22,6 +22,7 @@ def setUp(self): self.destination = self.root / "sdk/share/licenses/native" self.prefix = self.root / "provider" self.owners = {} + self.licenses = {} self.calls = [] def file(self, name, contents="notice\n"): @@ -38,6 +39,11 @@ def library(self, name="libsample.so.1", package="sample", manager="dpkg-query") def query(self, command, **kwargs): self.calls.append(command) + if command[:2] == ["pacman", "-Qi"]: + self.assertEqual(kwargs["env"]["LC_ALL"], "C") + licenses = self.licenses.get(command[-1]) + stdout = f"Name : {command[-1]}\nLicenses : {licenses}\nGroups : None\n" if licenses else "" + return subprocess.CompletedProcess(command, 0 if licenses else 1, stdout=stdout) package = self.owners.get((command[0], command[-1])) stdout = f"{package}: {command[-1]}\n" if command[0] == "dpkg-query" else f"{package}\n" return subprocess.CompletedProcess(command, 0 if package else 1, stdout=stdout if package else "") @@ -63,6 +69,49 @@ def test_arch(self): result = self.collect([library]) self.assertEqual(result["libraries"][0]["notices"], ["sample/licenses/LICENSE", "sample/licenses/extra/NOTICE"]) self.assertTrue(any(command[0] == "pacman" for command in self.calls)) + self.assertFalse(any(command[:2] == ["pacman", "-Qi"] for command in self.calls)) + + def test_arch_common(self): + libraries = [self.library("libdw.so.1", package="libelf", manager="pacman"), + self.library("libelf.so.1", package="libelf", manager="pacman"), + self.library("libglib-2.0.so.0", package="glib2", manager="pacman")] + self.licenses = {"libelf": "GPL-2.0-or-later OR LGPL-3.0-or-later", "glib2": "LGPL-2.1-or-later"} + for name in ("GPL-2.0-or-later", "LGPL-3.0-or-later", "LGPL-2.1-or-later", "AGPL-3.0-only"): + self.file(f"system/usr/share/licenses/spdx/{name}.txt", name + " license\n") + result = self.collect(libraries) + records = {record["library"]: record for record in result["libraries"]} + for library in ("libdw.so.1", "libelf.so.1"): + self.assertEqual(records[library]["notices"], + ["libelf/licenses/GPL-2.0-or-later.txt", "libelf/licenses/LGPL-3.0-or-later.txt"]) + self.assertEqual(records[library]["package"], "libelf") + self.assertEqual(records[library]["origin"], "installed-package") + self.assertEqual(records["libglib-2.0.so.0"]["notices"], ["glib2/licenses/LGPL-2.1-or-later.txt"]) + self.assertEqual((self.destination / "glib2/licenses/LGPL-2.1-or-later.txt").read_text(), + "LGPL-2.1-or-later license\n") + self.assertEqual(len(list(self.destination.rglob("*.txt"))), 3) + self.assertNotIn(str(self.root), json.dumps(result)) + + def test_arch_expression(self): + library = self.library(manager="pacman") + self.licenses["sample"] = "(GPL-3.0-or-later WITH GCC-exception-3.1)\n AND LGPL-2.1-or-later" + names = ("GPL-3.0-or-later", "exceptions/GCC-exception-3.1", "LGPL-2.1-or-later") + for name in names: + self.file(f"system/usr/share/licenses/spdx/{name}.txt", name + " text\n") + result = self.collect([library]) + self.assertEqual(result["libraries"][0]["notices"], sorted(f"sample/licenses/{name}.txt" for name in names)) + self.assertEqual((self.destination / "sample/licenses/exceptions/GCC-exception-3.1.txt").read_text(), + "exceptions/GCC-exception-3.1 text\n") + + def test_arch_incomplete(self): + library = self.library(manager="pacman") + self.file("system/usr/share/licenses/spdx/GPL-3.0-or-later.txt") + for expression in ("GPL-3.0-or-later OR LicenseRef-Missing", "GPL-3.0-or-later WITH Missing-exception", + "GPL-3.0-or-later OR ../GPL-3.0-or-later"): + with self.subTest(expression=expression): + self.licenses["sample"] = expression + with self.assertRaisesRegex(ValueError, "No native notices"): + self.collect([library]) + self.assertFalse(self.destination.exists()) def test_relative(self): library = self.library() diff --git a/docs/assets/asset-loading.md b/docs/assets/asset-loading.md index 358ae25c..dc27f7a2 100644 --- a/docs/assets/asset-loading.md +++ b/docs/assets/asset-loading.md @@ -155,7 +155,9 @@ replacements succeed; failed reload preserves its previous application pair even if the shared catalogue has published an earlier candidate. Fonts remain outside the graphics manifest contract. Manual material builders remain supported; the [rendering guide](../rendering/pipelines-and-materials.md#manual-material-construction) -shows the UI example that deliberately retains this path. +shows direct typed construction. TGUI and RmlUi use +[automatic provider-owned defaults](../rendering/pipelines-and-materials.md#default-ui-materials) +and accept compatible material handles from indexed recipes as optional overrides. `CE::Rendering::Resources::Provider::create_image()` accepts `CE::Assets::Images::Decoded` RGBA pixels. diff --git a/docs/assets/asset-manifests.md b/docs/assets/asset-manifests.md index c5d3aeb2..475cd467 100644 --- a/docs/assets/asset-manifests.md +++ b/docs/assets/asset-manifests.md @@ -227,7 +227,9 @@ The [loading guide](asset-loading.md) defines preparation, publication and expli replacement. [Pipelines and materials](../rendering/pipelines-and-materials.md) defines manual builders, sampling and retained generation behavior. Only the main text/image materials are indexed; the other shaders remain manual, unverified -examples. TGUI and RmlUi still construct their materials explicitly. +examples. TGUI and RmlUi select +[provider-owned default materials](../rendering/pipelines-and-materials.md#default-ui-materials) +automatically; custom UI materials can use the same indexed recipes described here. ## Recovered college definitions diff --git a/docs/development/ui-adapters.md b/docs/development/ui-adapters.md index f7ae14c5..964fd7d0 100644 --- a/docs/development/ui-adapters.md +++ b/docs/development/ui-adapters.md @@ -47,13 +47,20 @@ resolve the smallest demonstrated seam first. | Time, size and OS services | [Runtime ticks](../runtime/runtime-frame-boundary.md) and capability reporting | Use copied window/framebuffer dimensions and explicit simulation time; report unavailable clipboard, cursor and IME services. | | Queued upload and retained playback | [Thread dispatch](../runtime/thread-dispatch.md) and [native retirement](../resources/resource-residency.md#native-retirement-and-maintenance) | Send owned preparation through platform submission and retain complete published scenes through replacement and teardown. | -The application supplies backend-appropriate pipelines and materials. Both demo -UI integrations retain [manual construction](../rendering/pipelines-and-materials.md#manual-material-construction); -indexed loading currently migrates only main text/image materials. An adapter -validates their vertex layout, topology, alpha/depth/culling state, projection -semantic and texture parameters. It does not create OpenGL pipelines or query a -native context. Font rasterization may belong to the toolkit; its resulting atlas -still follows the same CPU-copy and resource-publication boundary. +Adapters request [built-in 2D materials](../rendering/pipelines-and-materials.md#default-ui-materials) +through the provider-owned `CE::Assets::Shaders::Builder`, selecting the toolkit's +alpha convention. Engine owns the generic profiles; each graphics backend owns +its shader sources, bindings and construction. OpenGL embeds those sources, so +applications need no UI shader files or native material builder. This uses the +existing provider capability without a dependency from the adapter to OpenGL. + +Applications may pass compatible material handles loaded from JSON or constructed +manually as explicit overrides. The adapter validates vertex layout, topology, +alpha/depth/culling state, projection semantic and texture parameters. It does not +create OpenGL pipelines or query a native context. Unsupported providers reject +default construction while explicit compatible overrides remain usable. Font +rasterization may belong to the toolkit; its resulting atlas still follows the +same CPU-copy and resource-publication boundary. Expanded triangles suffice for the current adapters. Indexing, batching and draw sorting require their own consumer need; preserve authored world/HUD/overlay order. @@ -76,8 +83,8 @@ The independent implementations differ where their toolkits differ: Existing Engine contracts cover both initial scopes. Their CPU recordings and uploaders remain toolkit-owned implementations; matching names do not justify a -common widget or session API. The application chooses compatible materials and -presentation order for each view. +common widget or session API. The application chooses presentation order and may +override default materials for each view. Unsupported renderer features need an explicit scope and failure/capability contract. Rectangular clipping does not establish arbitrary masks, filters, @@ -86,15 +93,16 @@ when an implemented consumer demonstrates the requirement. ## Update, publication and shutdown -1. On platform, prepare application materials and the resource upload endpoint. +1. On platform, prepare the resource upload endpoint. Default materials are + created lazily on that owner during the first nonempty upload. 2. On simulation/UI, create the toolkit session and native presentation objects. Keep all toolkit calls on that owner, including font and layout changes. 3. Each update supplies copied logical/framebuffer sizes, simulation time and the immutable input stream. Logical UI coordinates and framebuffer pixels remain distinct; zero drawable dimensions produce an empty recording. -4. Record owned CPU draw data, then submit it and retained materials for platform - upload. Toolkit vertices, pixel pointers, widgets and documents do not cross - that queue boundary. +4. Record owned CPU draw data, then submit it for platform upload, optionally + supplying retained custom material handles. Borrowed toolkit vertices, pixel + pointers, widgets and documents do not cross that queue boundary. 5. Adopt a replacement only when complete. A pending or failed replacement leaves the current scene usable. Bound outstanding replacements instead of queuing a new upload every tick. diff --git a/docs/engine-systems/gui.md b/docs/engine-systems/gui.md index 04fa8894..d0990c9e 100644 --- a/docs/engine-systems/gui.md +++ b/docs/engine-systems/gui.md @@ -29,7 +29,7 @@ their session, native authoring API and recording/publication types. | {cpp:struct}`TGUI::SessionOptions `, {cpp:struct}`RmlUi::SessionOptions ` | Set maximum texture size; TGUI also supplies explicit font scale. RmlUi uses native document styling. [Adapter ownership](../development/ui-adapters.md#module-and-application-ownership); [TGUI](../modules/tgui.md), [RmlUi](../modules/rmlui.md). | | {cpp:struct}`TGUI::Capabilities `, {cpp:struct}`RmlUi::Capabilities ` | Report keyboard focus, committed text, clipboard, cursor and IME availability; RmlUi also reports clipping/effects support. [Unavailable services](../development/ui-adapters.md#routing-and-unavailable-services). | | {cpp:enum}`CE::Input::Routing::KeyboardPolicy` and pointer selection | `Exclusive`/`PassThrough` controls keyboard gameplay. The application separately selects pointer recipients. [GUI routing](../gui/index.md#route-input-and-focus-explicitly). | -| {cpp:struct}`TGUI::Materials `, {cpp:struct}`RmlUi::Materials ` | Supply compatible layout, alpha, texture and projection contracts. [Bridge responsibilities](../development/ui-adapters.md#bridge-responsibilities). | +| {cpp:struct}`TGUI::Materials `, {cpp:struct}`RmlUi::Materials ` | Optionally override automatic defaults with compatible loaded material handles. [Custom UI materials](../rendering/pipelines-and-materials.md#custom-ui-materials). | | {cpp:struct}`TGUI::WidgetLayout ` | Typed placement/resizing for native TGUI widgets; RmlUi uses native RML/RCSS layout. [Choose an interface](../gui/index.md#choose-an-interface); [TGUI layout declarations](../../projects/modules/ui/tgui/include/cheryl/ui/tgui/layout.h). | ## Usage @@ -69,7 +69,8 @@ generations already retained by frames. ## Lifecycle -Prepare materials/upload helpers during platform-owned initialization. Create +Prepare the scene uploader during platform-owned initialization. It creates +default materials lazily through the selected resource provider. Create the toolkit session on its UI/simulation owner, then native objects. Update input, time and layout there, record CPU state, dispatch upload and retain the latest complete scene. During shutdown, stop UI calls and release application-held diff --git a/docs/engine-systems/rendering.md b/docs/engine-systems/rendering.md index d5d2c7e2..e814fe4b 100644 --- a/docs/engine-systems/rendering.md +++ b/docs/engine-systems/rendering.md @@ -24,6 +24,7 @@ Types below are in `CE::Rendering` unless another namespace is shown. | Type | Choices and details | | --- | --- | +| {cpp:enum}`Materials::Builtin2D ` | Backend-owned solid/textured colored-triangle profiles used by automatic UI defaults. [Default UI materials](../rendering/pipelines-and-materials.md#default-ui-materials). | | {cpp:enum}`Pipelines::Blend ` | `Opaque`, `StraightAlpha`, `PremultipliedAlpha`, `Additive`; match source color/alpha convention. [Pipeline and material state](../rendering/pipelines-and-materials.md#parameters-and-ownership). | | {cpp:enum}`Pipelines::Depth `, {cpp:enum}`Pipelines::Cull ` | Depth comparison and face culling; camera dimensionality does not select these policies. [Fixed state and pass constraints](../rendering/pipelines-and-materials.md#parameters-and-ownership). | | {cpp:enum}`Geometry::Layout2D `, {cpp:enum}`Geometry::Topology ` | Match position/UV or colored vertices and triangles/triangle strips to the pipeline. [Geometry and independent binding](../rendering/pipelines-and-materials.md#independent-binding). | diff --git a/docs/getting-started/create-a/hud.md b/docs/getting-started/create-a/hud.md index cf934011..c43c51f9 100644 --- a/docs/getting-started/create-a/hud.md +++ b/docs/getting-started/create-a/hud.md @@ -11,7 +11,7 @@ This tutorial replaces that score display with toolkit widgets. For document authoring, styling and interactive input through the other UI integration, see [Create an interactive GUI with RmlUi](../how-to/create-gui.md). -## Select TGUI and prepare its materials +## Select TGUI Enable `CHERYL_BUILD_UI_TGUI=ON` in First Game's source configuration, or use an SDK containing that module. Add the UI target to your game's existing links: @@ -24,25 +24,9 @@ Use your executable's target name in place of `game`. Prepare the pinned toolkit through [Project setup](../project-setup.md); enabling an option does not download the dependency. -Copy these application shaders into your asset root's `graphics/shaders/`: -[tgui.vert](../../../assets/graphics/shaders/tgui.vert), -[tgui-solid.frag](../../../assets/graphics/shaders/tgui-solid.frag) and -[tgui-textured.frag](../../../assets/graphics/shaders/tgui-textured.frag). -The existing indexed asset loader does not construct UI materials. - -In your game's `.cpp` file, include -``, ``, `` and -``, then place this helper before the game's method definitions: - -```{literalinclude} ../../../projects/apps/tutorial-game/src/controls.cpp -:language: cpp -:start-after: // [materials] -:end-before: // [/materials] -``` - -This helper creates compatible colored-triangle materials on the graphics -owner. TGUI uses straight alpha blending. Another graphics backend needs its -own compatible builder; the TGUI session has no OpenGL dependency. +The uploader obtains compatible rendering materials from the selected graphics +provider automatically. With OpenGL, their shaders are embedded in the library; +this HUD needs no additional shader files or material setup. ## Give the HUD its own class @@ -65,14 +49,12 @@ their simulation owner. #include #include #include -#include namespace Tutorial { class Hud final { CE::Input::System& input_; CE::Runtime::PlatformDispatcher::Submission platform_; CE::UI::TGUI::SceneUploader uploader_; - CE::UI::TGUI::Materials materials_; CE::UI::TGUI::Scene scene_; std::future pending_; std::unique_ptr session_; @@ -82,11 +64,10 @@ namespace Tutorial { unsigned int health_value_ = 100; public: - Hud(CE::Runtime::Context& engine, CE::UI::TGUI::Materials materials) + explicit Hud(CE::Runtime::Context& engine) : input_(engine.input()), platform_(engine.platform_dispatcher().submission()), - uploader_(engine.resources()), - materials_(std::move(materials)) {} + uploader_(engine.resources()) {} void set_score(int value) { score_value_ = value; } void set_health(unsigned int value) { health_value_ = std::min(value, 100u); } @@ -107,7 +88,7 @@ namespace Tutorial { static_cast(CE::UI::TGUI::adopt_scene(pending_, scene_)); if (!pending_.valid()) - pending_ = uploader_.submit(platform_, session_->record(), materials_); + pending_ = uploader_.submit(platform_, session_->record()); } void append(CE::Rendering::FrameWriter& frame) const { scene_.write(frame); } @@ -186,7 +167,7 @@ unsigned int health_ = 100; During platform-owned `init()`, after graphics initialization: ```cpp -hud_.emplace(engine_, materials(engine_.resources(), asset_root_)); +hud_.emplace(engine_); ``` After quit handling and gameplay updates, publish current game values to the @@ -220,8 +201,10 @@ are recorded when that request finishes. Ready upload failures propagate through the runtime's normal failure handling. Record and mutate widgets only on simulation. The upload captures owned scene -data and materials; published frames retain their selected graphics resources -without accessing the live widgets. +data and creates the default materials lazily on the platform owner. Published +frames retain their selected graphics resources without accessing live widgets. +To use custom shaders, pass loaded material handles through the optional +[`Materials` override](../../rendering/pipelines-and-materials.md#custom-ui-materials). ## Release and observe @@ -244,7 +227,7 @@ keys continue to work. Check both runtime modes for input and teardown behavior. The current [tutorial-game executable](../../../projects/apps/tutorial-game/README.md) uses a direct text score and a separate TGUI controls panel; it does not implement -this widget HUD. Its controls source supplies the material helper above. +this widget HUD. Its controls use the same automatic material defaults. The [TGUI demo](../../../projects/apps/demo/src/tgui-demo.cpp) also demonstrates interactive widgets. Read [TGUI contracts](../../modules/tgui.md) and [GUI ownership](../../gui/index.md) for the integration boundaries. diff --git a/docs/getting-started/how-to/create-gui.md b/docs/getting-started/how-to/create-gui.md index f91c90f3..c82dbe73 100644 --- a/docs/getting-started/how-to/create-gui.md +++ b/docs/getting-started/how-to/create-gui.md @@ -27,22 +27,15 @@ assets/ │ ├── DejaVuSans.ttf │ └── DejaVuSansLicense.txt └── graphics/ - ├── shaders/ - │ ├── tgui.vert - │ ├── tgui-solid.frag - │ └── tgui-textured.frag └── ui/ ├── tutorial.rml └── tutorial.rcss ``` Copy the [font](../../../assets/fonts/DejaVuSans.ttf) and -[its license](../../../assets/fonts/DejaVuSansLicense.txt), and the -[vertex](../../../assets/graphics/shaders/tgui.vert), -[solid](../../../assets/graphics/shaders/tgui-solid.frag) and -[textured](../../../assets/graphics/shaders/tgui-textured.frag) shaders from the -checkout. The shader filenames are shared with the TGUI integration; RmlUi uses -its own materials and premultiplied blending. +[its license](../../../assets/fonts/DejaVuSansLicense.txt) from the checkout. +The uploader obtains compatible rendering materials automatically. OpenGL embeds +their shaders in the library, so this interface needs no additional shader files. The existing First Game assets are still used. RmlUi loads its documents, styles and fonts explicitly, outside the graphics manifest loader. @@ -105,12 +98,11 @@ This example uses rectangular drawing and clipping. Check the [RmlUi capability contract](../../modules/rmlui.md#recording-and-publication) before adding transforms, masks or effects. -## Prepare compatible materials on platform +## Include the UI interfaces Add these includes to the appropriate game header/source files: ```cpp -#include #include #include #include @@ -131,42 +123,6 @@ Add these includes to the appropriate game header/source files: #include ``` -Place this helper in your game's `.cpp` file before its method definitions: - -```cpp -CE::UI::RmlUi::Materials make_rmlui_materials( - CE::Rendering::Resources::Provider& provider, const std::filesystem::path& root -) { - auto* native = dynamic_cast(&provider); - if (!native) - throw std::runtime_error("This UI material builder requires OpenGL"); - CE::Rendering::Pipelines::Definition definition; - definition.vertex_layout = CE::Rendering::Geometry::Layout2D::Position3UV2Color4; - definition.topology = CE::Rendering::Geometry::Topology::Triangles; - definition.state = { - CE::Rendering::Pipelines::Blend::PremultipliedAlpha, CE::Rendering::Pipelines::Depth::Disabled, false, - CE::Rendering::Pipelines::Cull::None - }; - definition.parameters = { - {"projection", CE::Rendering::Parameters::Type::Mat4, true, CE::Rendering::Parameters::Semantic::Projection} - }; - definition.program_sources = {root / "graphics/shaders/tgui.vert", root / "graphics/shaders/tgui-solid.frag"}; - const CE::Rendering::OpenGL::PipelineBindings solid{{{"projection", "projectionMatrix"}}}; - CE::UI::RmlUi::Materials result; - result.solid = native->build_material({native->build_pipeline(definition, solid), {}}); - definition.program_sources[1] = root / "graphics/shaders/tgui-textured.frag"; - definition.parameters.push_back({"image", CE::Rendering::Parameters::Type::Sampler2D}); - const CE::Rendering::OpenGL::PipelineBindings textured{{{"projection", "projectionMatrix"}, {"image", "mytexture"}}}; - result.textured = native->build_material({native->build_pipeline(std::move(definition), textured), {}}); - return result; -} -``` - -RmlUi supplies premultiplied pixels and vertex colors. These shaders multiply -image RGBA by vertex RGBA, and the material blends with `PremultipliedAlpha`. -Using the TGUI helper's straight alpha state would violate this contract. -Another renderer needs an equivalent builder on its graphics owner. - ## Retain application state and an event listener Define this small listener before the game class in its header. It records an @@ -182,12 +138,11 @@ public: }; ``` -Add these game members, alongside First Game's `engine_` and `asset_root_`: +Add these game members after First Game's `engine_` and `asset_root_` members: ```cpp -CE::Runtime::PlatformDispatcher::Submission ui_platform_; +CE::Runtime::PlatformDispatcher::Submission ui_platform_{engine_.platform_dispatcher().submission()}; std::optional ui_uploader_; -CE::UI::RmlUi::Materials ui_materials_; CE::UI::RmlUi::Scene ui_scene_; std::future ui_pending_; std::unique_ptr ui_; @@ -207,13 +162,12 @@ Keep native objects after the session in member declaration order. During platform-owned `init()`, after graphics initialization: ```cpp -ui_platform_ = engine_.platform_dispatcher().submission(); ui_uploader_.emplace(engine_.resources()); -ui_materials_ = make_rmlui_materials(engine_.resources(), asset_root_); ``` The uploader belongs to the resource provider's platform owner. Upload work -uses owned recordings; live document operations stay on the UI owner. +uses owned recordings and creates defaults lazily on that platform owner; live +document operations stay on the UI owner. ## Load the document on the first update @@ -320,15 +274,19 @@ At the end of update: ```cpp static_cast(CE::UI::RmlUi::adopt_scene(ui_pending_, ui_scene_)); if (!ui_pending_.valid()) - ui_pending_ = ui_uploader_->submit(ui_platform_, ui_->record(), ui_materials_); + ui_pending_ = ui_uploader_->submit(ui_platform_, ui_->record()); ``` Adoption does not wait on pending work. Keep the previous complete scene while -one replacement waits; only owned recording/material data crosses to graphics. +one replacement waits; only owned recording data crosses to graphics. Ready upload errors propagate here through runtime's normal failure cleanup. The maintained demo instead catches them to report the error and keep its last complete scene. +The defaults preserve RmlUi's premultiplied alpha convention. To use custom +shaders, load compatible materials from JSON and pass their handles through the +optional [`Materials` override](../../rendering/pipelines-and-materials.md#custom-ui-materials). + After your world/HUD passes in `prepare_render_frame()`: ```cpp @@ -356,7 +314,6 @@ if (ui_) { } ui_scene_ = CE::UI::RmlUi::Scene{}; ui_uploader_.reset(); -ui_materials_ = CE::UI::RmlUi::Materials{}; ``` Keep the game's other partial-safe cleanup. The listener must detach and die diff --git a/docs/gui/index.md b/docs/gui/index.md index 7c8886da..f083ff19 100644 --- a/docs/gui/index.md +++ b/docs/gui/index.md @@ -22,7 +22,8 @@ retained scenes; Engine headers do not import either toolkit. Create the session before toolkit objects, on the thread that will own its UI updates. In concurrent runtime mode that is the simulation thread, so create it on the first update rather than during platform-owned game initialization. -Construct platform-owned materials/upload helpers during `init()` instead. +Construct the scene uploader from the resource provider during platform-owned +`init()`. It obtains default rendering materials automatically during upload. For TGUI, a session is constructed with the input system and a distinct nonzero focus target. A native button and edit field are ordinary toolkit objects: @@ -46,7 +47,7 @@ callback's target state. Include the toolkit's Button/EditBox headers and link the selected UI target. It creates an interactive interface; upload/publication is still required before it appears on screen. The maintained [TGUI demo](../../projects/apps/demo/src/tgui-demo.cpp) supplies the complete -material, widget and scene wiring. +widget and scene wiring. ## Route input and focus explicitly @@ -77,9 +78,13 @@ a toolkit's independent main loop over Cheryl's runtime. Recording creates owned CPU scene data. Send upload work to the platform through its dispatcher, consume completed results without blocking simulation, and -append the latest uploaded scene to a prepared frame. The TGUI and RmlUi owner -guides specify their material and scene interfaces. Each frame retains its own -published resources; later UI edits must not mutate an earlier frame's data. +append the latest uploaded scene to a prepared frame. Both integrations support +`uploader.submit(platform, session.record())`; their default materials are created +lazily on the provider owner. OpenGL embeds the shaders, so the UI needs no +application material builder or additional shader assets. The optional third +argument accepts [custom material handles](../rendering/pipelines-and-materials.md#custom-ui-materials). +Each frame retains its published resources; later UI edits must not mutate an +earlier frame's data. After simulation joins, release application-held widget, document, font and texture references before session teardown. `close_after_quiescence()` permits diff --git a/docs/planning/develop-review-and-development-plan.md b/docs/planning/develop-review-and-development-plan.md index be1f16a4..2eda0e5a 100644 --- a/docs/planning/develop-review-and-development-plan.md +++ b/docs/planning/develop-review-and-development-plan.md @@ -62,8 +62,9 @@ Remaining acceptance has independent prerequisites: | Demo material/artwork QA | Complete shader-pair failure/recovery and available artwork observations in [TR19](../testing-requests.md#tr19-qa-indexed-demo-materials-and-recovered-graphics). Missing images/showcase fixtures remain explicit blockers. | Sampling/TGUI and indexed shader/material loading have selected Linux CPU/backend -and native pixel acceptance. Only main text/image materials migrated; both UI -adapters retain [manual builders](../rendering/pipelines-and-materials.md#manual-material-construction). +and native pixel acceptance. Main text/image materials use indexed recipes; +UI adapters use [provider-owned defaults](../rendering/pipelines-and-materials.md#default-ui-materials) +and accept optional custom material handles. The reusable [graphics procedure](../development/architecture-validation.md#sampling-and-positioned-text) preserves coverage limits. Completed implementation plans are retired after their contracts and unresolved acceptance have moved to the subject guides/testing queue. diff --git a/docs/rendering/pipelines-and-materials.md b/docs/rendering/pipelines-and-materials.md index 7a104e26..6609df95 100644 --- a/docs/rendering/pipelines-and-materials.md +++ b/docs/rendering/pipelines-and-materials.md @@ -10,7 +10,9 @@ can retain resources without keeping a closed graphics backend usable. | --- | --- | | Draw a sprite with the maintained assets/materials | [Load and draw a sprite](../getting-started/how-to/draw-sprite.md), then the CPU submission contract below. | | Load authored shader/material recipes | [Shader/material manifests](../assets/asset-manifests.md#shadermaterial-definitions) through the asset loader. | -| Construct application-specific UI materials | [Manual material construction](#manual-material-construction), on the graphics owner. | +| Render a toolkit UI | [Default UI materials](#default-ui-materials), selected automatically by its uploader. | +| Customize UI rendering | [Custom UI materials](#custom-ui-materials), using loaded material handles. | +| Construct a material directly | [Manual material construction](#manual-material-construction), on the graphics owner. | | Change filtering for one image binding | [Nearest-sampling example](#choose-nearest-sampling-for-pixel-art), retaining a sampler separately from image pixels. | ## CPU submission @@ -352,13 +354,85 @@ reflection/type/storage failures, invalid attributes/unlinked programs, and samp domain/unit failures before any bind. These sources use synthetic IDs and restore all replaced entry points; they do not replace real-context acceptance. +## Default UI materials + +TGUI and RmlUi uploaders select compatible materials when called without a +`Materials` argument: + +```cpp +pending = uploader.submit(platform, session.record()); +``` + +Construct the uploader on the provider owner during initialization. The first +nonempty upload requests defaults on that owner, and subsequent uploads retain +and reuse them. Empty recordings need no material capability. TGUI selects +`StraightAlpha`; RmlUi selects `PremultipliedAlpha`. Applications need no material +factory or additional shader files for these defaults with OpenGL. + +The generic contract is `CE::Assets::Shaders::Builder::build_builtin_material(kind, blend)`, +accessed through `provider.shader_asset_builder()`. Engine defines +`CE::Rendering::Materials::Builtin2D`; the graphics backend owns its realization: + +| Profile | Contract | +| --- | --- | +| `SolidColor` | Output vertex RGBA from `Position3UV2Color4` triangles. | +| `TexturedColor` | Multiply sampled RGBA by vertex RGBA, with a required custom `Sampler2D` named `image`. Preserve the caller's alpha convention. | + +Both use the required `Mat4` projection semantic named `projection`, disabled +depth/write/culling and the requested blend state. OpenGL embeds its shader +sources and uniform mappings and constructs them through its existing validated +recipe path. Its provider weakly caches programs by profile and materials by +profile/blend. Different alpha variants share the live program without changing +each other's state; uploader handles and published frames keep resources alive. +The cache adds no global resource owner or catalogue entries. + +Construction requires the provider's live domain, platform owner and current +graphics context, including a warm cache lookup. A provider without a shader +builder, or a builder without built-in support, rejects the default request. +UI modules depend on the generic Engine contract; they do not link OpenGL. + +### Custom UI materials + +To customize rendering, author and load +[shader/material JSON recipes](../assets/asset-manifests.md#shadermaterial-definitions), +then pass their material handles to the uploader. No application material factory +is needed. For example, after loading `game:ui-solid` and `game:ui-textured`, select +them during initialization: + +```cpp +CE::UI::TGUI::Materials custom; +auto& catalogue = CE::Assets::Shaders::Manager::get(); +custom.solid = catalogue.find_material("game:ui-solid"); +custom.textured = catalogue.find_material("game:ui-textured"); +if (!custom.solid || !custom.textured) + throw std::runtime_error("Custom UI materials are required"); +custom.image_parameter = "image"; +``` + +Include ``, +`` and ``. Retain `custom` with the view and +pass it when publishing a recording: + +```cpp +pending = uploader.submit(platform, session.record(), custom); +``` + +RmlUi uses its own `CE::UI::RmlUi::Materials` with the same handle-selection +pattern. Overrides must match the adapter's colored triangles, projection +semantic, image parameter and alpha/depth/culling contract. Choose the toolkit's +blend state in the JSON; RmlUi's sampled/vertex RGBA must stay premultiplied. +`image_parameter` and `image_unit` select the custom sampler key and binding unit. +Other custom values come from material defaults or `Materials::pass_parameters`. +The third argument bypasses default construction; explicitly passing empty or +incompatible handles fails for draws that use them. It does not trigger fallback. +Published packets retain the chosen generations through later catalogue changes. + ## Manual material construction -TGUI and RmlUi deliberately keep manual recipes. The demo's -[TGUI builder](../../projects/apps/demo/src/tgui-demo.cpp) and -[RmlUi builder](../../projects/apps/demo/src/rmlui-demo.cpp) show adding typed -definitions and wiring their bindings explicitly. On the OpenGL loading owner, -with its context current, a textured TGUI material can be built as follows +Direct typed construction remains available independently of UI defaults and +indexed loading. Use it when code must construct a recipe explicitly; the normal +custom UI path above can use JSON. On the OpenGL loading owner, with its context +current, a textured TGUI material can be built as follows (`native` is a `CE::Rendering::OpenGL::ResourceProvider&`, `root` is the asset root): ```cpp @@ -385,7 +459,6 @@ the same construction pattern with premultiplied-alpha blending. Required parame keys belong in the typed definition, and their uniform mappings belong in bindings; adding one requires both and a matching shader uniform. -A future indexed UI migration targets TGUI first. If RmlUi also migrates, retain -this manual example and an independently usable manual consumer. Other shader -files are not indexed merely because they exist; their purposes and native behavior -remain unverified. +Keep this manual API, example and its consumer coverage usable independently of +automatic UI defaults. Other shader files are not indexed merely because they +exist; their purposes and native behavior remain unverified. diff --git a/docs/resources/resource-residency.md b/docs/resources/resource-residency.md index ee7c9809..f56df329 100644 --- a/docs/resources/resource-residency.md +++ b/docs/resources/resource-residency.md @@ -60,6 +60,10 @@ the table are relative to `CE`. | Pipeline/material | `Rendering::OpenGL::Pipeline` retains its program, contract defaults and backend missing-value bindings; `Rendering::Resources::Material` retains its pipeline, default parameter bindings and FX sampler overrides. Resolved values retain image/sampler handles; frames retain immutable material generations. | Native handles retire through the same program/image/sampler owners. Successful replacement preserves old frame owners; failed reload preserves the cache entry. | | Indexed shader catalogue | `Assets::Shaders::Manager` retains CPU recipes paired with executable programs/materials; material entries retain their selected program generation | Catalogue replacement/clear drops its owners; native resources retire through their underlying handles. | +UI uploaders retain their lazily created default material pair. OpenGL's built-in +program/material caches are weak; they do not add catalogue entries or prolong +residency after uploader, scene and frame handles release those resources. + `CE::Rendering::OpenGL::Handle` registration is move-only. Destruction from any thread marks its registration pending; it never calls GL. Duplicate pending retirement is ignored, and collected registration slots can be reused only after deletion. Renderer diff --git a/projects/apps/demo/README.md b/projects/apps/demo/README.md index 257083aa..6e72f521 100644 --- a/projects/apps/demo/README.md +++ b/projects/apps/demo/README.md @@ -55,8 +55,10 @@ The builtin HUD selects fonts by name from the asset root, then system directori and always retains an embedded fallback; it can start without an installed system font. An asset font overrides a system font with the same name. RmlUi keeps its separate file-based service, using a selected file or the checked-in DejaVu Sans -fallback. TGUI uses its own embedded default font. Shaders and the small RmlUi proof document/images are checked in under the -asset root. The tile/sprite showcase attempts to load three optional package images +fallback. TGUI uses its own embedded default font. Main text/image shaders and the +small RmlUi proof document/images are checked in under the asset root. UI shaders +are embedded in the OpenGL library and selected automatically. The tile/sprite +showcase attempts to load three optional package images on every launch. Missing or broken package metadata/images skip the affected samples and report once to stderr; the HUD, input and selected UI views continue running. `--full-assets` also attempts the whole manifest/image tree and reports a failed @@ -236,7 +238,7 @@ example exercises replacement rather than Chinese-language coverage. ## UI ownership and uploads `Game` passes the neutral `DemoUiStatus` model to its selected views and receives -camera-reset actions. Each view keeps manual OpenGL material construction on platform during +camera-reset actions. Each view prepares its uploader on platform during initialization, then creates its session/native objects lazily on the first simulation update. It supplies copied logical/framebuffer dimensions and simulation time. Complete CPU recordings go through the platform dispatcher/uploader, with @@ -245,12 +247,13 @@ scene and appears in the HUD. Frame preparation appends TGUI, then RmlUi, after the world/HUD pass; it never traverses live widgets. Teardown follows simulation join and releases application-held widgets/elements/listeners before their toolkit globals. Both adapters remain independent; their app views share only the status -model and generic shader assets, with distinct alpha pipelines. The -[manual recipe example](../../../docs/rendering/pipelines-and-materials.md#manual-material-construction) -explains definition/binding wiring. A future indexed UI migration targets TGUI -first; RmlUi retains a separate manual example if it later migrates too. Each view delivers -the complete input batch once through its session's control callback, retaining -the entry keyboard epoch while visibility controls select pointer delivery per record. +model. Uploads select [automatic materials](../../../docs/rendering/pipelines-and-materials.md#default-ui-materials) +through the generic provider contract. OpenGL owns embedded shaders and reuses +their programs across the distinct straight/premultiplied alpha pipelines. +Custom materials can be loaded from JSON and passed as explicit handles. +Each view delivers the complete input batch once through its session's control +callback, retaining the entry keyboard epoch while visibility controls select +pointer delivery per record. The demo uses the adapter's [typed layout](../../modules/ui/tgui/README.md#typed-layout). The panel anchors to the window's top-right with a 24-unit inset. Its width follows diff --git a/projects/apps/demo/src/demo-game.cpp b/projects/apps/demo/src/demo-game.cpp index 09ac29ad..1a191446 100644 --- a/projects/apps/demo/src/demo-game.cpp +++ b/projects/apps/demo/src/demo-game.cpp @@ -196,7 +196,7 @@ class GameImpl : public CE::Game::AbstractGame { (void)bindings.bind_button({input.gamepad_id(), CE::Input::GamepadControl::A}, DemoActions::GamepadA); events_ = input.capture(CE::Input::Capture::Channel::Events); #ifdef CHERYL_DEMO_TGUI - ui_ = std::make_unique(engine_, asset_root_); + ui_ = std::make_unique(engine_); #endif #ifdef CHERYL_DEMO_RMLUI // RmlUi retains its separate file-based font service. diff --git a/projects/apps/demo/src/rmlui-demo.cpp b/projects/apps/demo/src/rmlui-demo.cpp index 59ceaf49..ec5b5ea6 100644 --- a/projects/apps/demo/src/rmlui-demo.cpp +++ b/projects/apps/demo/src/rmlui-demo.cpp @@ -1,6 +1,5 @@ #include "rmlui-demo.h" -#include #include #include #include @@ -18,31 +17,6 @@ #include namespace { - CE::UI::RmlUi::Materials materials(CE::Rendering::Resources::Provider& provider, const std::filesystem::path& root) { - auto* native = dynamic_cast(&provider); - if (!native) - throw CE::Exceptions::invalid_args(CE_HERE, "The demo UI requires an OpenGL material provider"); - CE::Rendering::Pipelines::Definition definition; - definition.vertex_layout = CE::Rendering::Geometry::Layout2D::Position3UV2Color4; - definition.state = { - CE::Rendering::Pipelines::Blend::PremultipliedAlpha, CE::Rendering::Pipelines::Depth::Disabled, false, - CE::Rendering::Pipelines::Cull::None - }; - definition.parameters = { - {"projection", CE::Rendering::Parameters::Type::Mat4, true, CE::Rendering::Parameters::Semantic::Projection} - }; - // These application shaders multiply vertex/image RGBA for both toolkits. - // RmlUi's pipeline preserves its premultiplied output through blending. - definition.program_sources = {root / "graphics/shaders/tgui.vert", root / "graphics/shaders/tgui-solid.frag"}; - const CE::Rendering::OpenGL::PipelineBindings solid_bindings{{{"projection", "projectionMatrix"}}}; - CE::UI::RmlUi::Materials result; - result.solid = native->build_material({native->build_pipeline(definition, solid_bindings), {}}); - definition.program_sources[1] = root / "graphics/shaders/tgui-textured.frag"; - definition.parameters.push_back({"image", CE::Rendering::Parameters::Type::Sampler2D}); - const CE::Rendering::OpenGL::PipelineBindings textured_bindings{{{"projection", "projectionMatrix"}, {"image", "mytexture"}}}; - result.textured = native->build_material({native->build_pipeline(std::move(definition), textured_bindings), {}}); - return result; - } class Callback final : public Rml::EventListener { std::function action_; @@ -65,7 +39,6 @@ struct DemoRmlUi::RmlUiState { CE::Input::System& input; CE::Runtime::PlatformDispatcher::Submission platform; CE::UI::RmlUi::SceneUploader uploader; - CE::UI::RmlUi::Materials materials; const std::filesystem::path root; const std::filesystem::path font; std::unique_ptr session; @@ -87,7 +60,6 @@ struct DemoRmlUi::RmlUiState { : input(engine.input()), platform(engine.platform_dispatcher().submission()), uploader(engine.resources()), - materials(::materials(engine.resources(), root)), root(root), font(std::move(font)) {} @@ -205,7 +177,7 @@ bool DemoRmlUi::update(const CE::Game::TickContext& tick, const DemoUiStatus& st if (CE::UI::RmlUi::adopt_scene(rmlui.pending, rmlui.scene)) rmlui.upload_error.clear(); if (!rmlui.pending.valid()) - rmlui.pending = rmlui.uploader.submit(rmlui.platform, rmlui.session->record(), rmlui.materials); + rmlui.pending = rmlui.uploader.submit(rmlui.platform, rmlui.session->record()); } catch (const std::exception& error) { rmlui.upload_error = error.what(); } diff --git a/projects/apps/demo/src/tgui-demo.cpp b/projects/apps/demo/src/tgui-demo.cpp index c58d4a2f..69047c57 100644 --- a/projects/apps/demo/src/tgui-demo.cpp +++ b/projects/apps/demo/src/tgui-demo.cpp @@ -1,6 +1,5 @@ #include "tgui-demo.h" -#include #include #include #include @@ -22,30 +21,6 @@ #include namespace { - CE::UI::TGUI::Materials materials(CE::Rendering::Resources::Provider& provider, const std::filesystem::path& root) { - auto* native = dynamic_cast(&provider); - if (!native) - throw CE::Exceptions::invalid_args(CE_HERE, "The demo UI requires an OpenGL material provider"); - CE::Rendering::Pipelines::Definition definition; - definition.vertex_layout = CE::Rendering::Geometry::Layout2D::Position3UV2Color4; - definition.topology = CE::Rendering::Geometry::Topology::Triangles; - definition.state = { - CE::Rendering::Pipelines::Blend::StraightAlpha, CE::Rendering::Pipelines::Depth::Disabled, false, - CE::Rendering::Pipelines::Cull::None - }; - definition.parameters = { - {"projection", CE::Rendering::Parameters::Type::Mat4, true, CE::Rendering::Parameters::Semantic::Projection} - }; - definition.program_sources = {root / "graphics/shaders/tgui.vert", root / "graphics/shaders/tgui-solid.frag"}; - const CE::Rendering::OpenGL::PipelineBindings solid_bindings{{{"projection", "projectionMatrix"}}}; - CE::UI::TGUI::Materials result; - result.solid = native->build_material({native->build_pipeline(definition, solid_bindings), {}}); - definition.program_sources[1] = root / "graphics/shaders/tgui-textured.frag"; - definition.parameters.push_back({"image", CE::Rendering::Parameters::Type::Sampler2D}); - const CE::Rendering::OpenGL::PipelineBindings textured_bindings{{{"projection", "projectionMatrix"}, {"image", "mytexture"}}}; - result.textured = native->build_material({native->build_pipeline(std::move(definition), textured_bindings), {}}); - return result; - } tgui::Texture image(const bool alternate) { constexpr unsigned int extent = 16; @@ -78,7 +53,6 @@ struct DemoUi::TguiState { CE::Input::System& input; CE::Runtime::PlatformDispatcher::Submission platform; CE::UI::TGUI::SceneUploader uploader; - CE::UI::TGUI::Materials materials; // External widget handles are declared after session and destroyed first. std::unique_ptr session; tgui::Panel::Ptr panel; @@ -93,11 +67,8 @@ struct DemoUi::TguiState { bool reset_requested = false; bool alternate_image = false; - TguiState(CE::Runtime::Context& engine, const std::filesystem::path& root) - : input(engine.input()), - platform(engine.platform_dispatcher().submission()), - uploader(engine.resources()), - materials(::materials(engine.resources(), root)) {} + explicit TguiState(CE::Runtime::Context& engine) + : input(engine.input()), platform(engine.platform_dispatcher().submission()), uploader(engine.resources()) {} void create_widgets(const CE::Game::TickContext& tick) { using CE::UI::TGUI::Scalable; @@ -244,8 +215,8 @@ struct DemoUi::TguiState { } }; -DemoUi::DemoUi(CE::Runtime::Context& engine, const std::filesystem::path& asset_root) -: tgui_state_(std::make_unique(engine, asset_root)) {} +DemoUi::DemoUi(CE::Runtime::Context& engine) +: tgui_state_(std::make_unique(engine)) {} DemoUi::~DemoUi() = default; @@ -268,7 +239,7 @@ bool DemoUi::update(const CE::Game::TickContext& tick, const DemoUiStatus& statu tgui.upload_error.clear(); // No queue growth: keep one complete scene while its replacement waits. if (!tgui.pending.valid()) - tgui.pending = tgui.uploader.submit(tgui.platform, tgui.session->record(), tgui.materials); + tgui.pending = tgui.uploader.submit(tgui.platform, tgui.session->record()); } catch (const std::exception& error) { tgui.upload_error = error.what(); // The last complete scene remains usable. } diff --git a/projects/apps/demo/src/tgui-demo.h b/projects/apps/demo/src/tgui-demo.h index b8724316..377c03d7 100644 --- a/projects/apps/demo/src/tgui-demo.h +++ b/projects/apps/demo/src/tgui-demo.h @@ -6,7 +6,6 @@ #include #include -#include #include #include @@ -20,8 +19,8 @@ class DemoUi final { std::unique_ptr tgui_state_; public: - // Build application materials on platform, without creating toolkit objects. - DemoUi(CE::Runtime::Context& engine, const std::filesystem::path& asset_root); + // Prepare upload dependencies on platform, without creating toolkit objects. + explicit DemoUi(CE::Runtime::Context& engine); // Runtime calls deinit after joining simulation; all widget refs die first. ~DemoUi(); DemoUi(const DemoUi&) = delete; diff --git a/projects/apps/tutorial-game/CMakeLists.txt b/projects/apps/tutorial-game/CMakeLists.txt index d43a7416..a46b4abb 100644 --- a/projects/apps/tutorial-game/CMakeLists.txt +++ b/projects/apps/tutorial-game/CMakeLists.txt @@ -45,7 +45,4 @@ target_sources(tutorial-game PRIVATE assets/graphics/shaders/materials.json assets/graphics/shaders/first-game.vert assets/graphics/shaders/first-game.frag - assets/graphics/shaders/tgui.vert - assets/graphics/shaders/tgui-solid.frag - assets/graphics/shaders/tgui-textured.frag assets/graphics/textures/actor.png) diff --git a/projects/apps/tutorial-game/README.md b/projects/apps/tutorial-game/README.md index c4749f13..749ed797 100644 --- a/projects/apps/tutorial-game/README.md +++ b/projects/apps/tutorial-game/README.md @@ -5,9 +5,9 @@ with input, sprite animation, camera movement, a direct text score and TGUI controls. The [focused tasks](../../../docs/getting-started/how-to/index.md) explain its input, sprite, camera and text operations. It consumes the standard Startup module, Native GLFW, OpenGL and TGUI. The two-cell actor and base -image/text shaders are copied from First Game; the colored UI shaders come from -the main demo. Its small asset set is listed explicitly in CMake and needs no -optional external art download. +image/text shaders are copied from First Game; TGUI uses the selected provider's +automatic materials with embedded OpenGL shaders. Its small asset set is listed +explicitly in CMake and needs no optional external art download. The [TGUI widget HUD tutorial](../../../docs/getting-started/create-a/hud.md) and [RmlUi GUI guide](../../../docs/getting-started/how-to/create-gui.md) describe diff --git a/projects/apps/tutorial-game/assets/graphics/shaders/tgui-solid.frag b/projects/apps/tutorial-game/assets/graphics/shaders/tgui-solid.frag deleted file mode 100644 index bae79935..00000000 --- a/projects/apps/tutorial-game/assets/graphics/shaders/tgui-solid.frag +++ /dev/null @@ -1,9 +0,0 @@ -#version 330 core - -in vec4 v_color; -out vec4 out_Color; - -void main() -{ - out_Color = v_color; -} diff --git a/projects/apps/tutorial-game/assets/graphics/shaders/tgui-textured.frag b/projects/apps/tutorial-game/assets/graphics/shaders/tgui-textured.frag deleted file mode 100644 index cd09af8f..00000000 --- a/projects/apps/tutorial-game/assets/graphics/shaders/tgui-textured.frag +++ /dev/null @@ -1,12 +0,0 @@ -#version 330 core - -uniform sampler2D mytexture; - -in vec2 v_texcoord; -in vec4 v_color; -out vec4 out_Color; - -void main() -{ - out_Color = texture(mytexture, v_texcoord) * v_color; -} diff --git a/projects/apps/tutorial-game/assets/graphics/shaders/tgui.vert b/projects/apps/tutorial-game/assets/graphics/shaders/tgui.vert deleted file mode 100644 index 58689b72..00000000 --- a/projects/apps/tutorial-game/assets/graphics/shaders/tgui.vert +++ /dev/null @@ -1,17 +0,0 @@ -#version 330 core - -uniform mat4 projectionMatrix; - -layout(location = 0) in vec3 in_Position; -layout(location = 1) in vec2 in_Texcoord; -layout(location = 2) in vec4 in_Color; - -out vec2 v_texcoord; -out vec4 v_color; - -void main() -{ - gl_Position = projectionMatrix * vec4(in_Position, 1.0); - v_texcoord = in_Texcoord; - v_color = in_Color; -} diff --git a/projects/apps/tutorial-game/src/controls.cpp b/projects/apps/tutorial-game/src/controls.cpp index 4b68ee4d..5e769625 100644 --- a/projects/apps/tutorial-game/src/controls.cpp +++ b/projects/apps/tutorial-game/src/controls.cpp @@ -1,49 +1,15 @@ #include "controls.h" -#include #include #include -#include #include #include -namespace { - // [materials] - CE::UI::TGUI::Materials materials(CE::Rendering::Resources::Provider& provider, const std::filesystem::path& root) { - auto* native = dynamic_cast(&provider); - if (!native) - throw std::invalid_argument("Tutorial UI requires the selected OpenGL resource provider"); - CE::Rendering::Pipelines::Definition definition; - definition.vertex_layout = CE::Rendering::Geometry::Layout2D::Position3UV2Color4; - definition.topology = CE::Rendering::Geometry::Topology::Triangles; - definition.state = { - CE::Rendering::Pipelines::Blend::StraightAlpha, CE::Rendering::Pipelines::Depth::Disabled, false, - CE::Rendering::Pipelines::Cull::None - }; - definition.parameters = { - {"projection", CE::Rendering::Parameters::Type::Mat4, true, CE::Rendering::Parameters::Semantic::Projection} - }; - definition.program_sources = {root / "graphics/shaders/tgui.vert", root / "graphics/shaders/tgui-solid.frag"}; - const CE::Rendering::OpenGL::PipelineBindings solid_bindings{{{"projection", "projectionMatrix"}}}; - CE::UI::TGUI::Materials result; - result.solid = native->build_material({native->build_pipeline(definition, solid_bindings), {}}); - definition.program_sources[1] = root / "graphics/shaders/tgui-textured.frag"; - definition.parameters.push_back({"image", CE::Rendering::Parameters::Type::Sampler2D}); - const CE::Rendering::OpenGL::PipelineBindings textured_bindings{{{"projection", "projectionMatrix"}, {"image", "mytexture"}}}; - result.textured = native->build_material({native->build_pipeline(std::move(definition), textured_bindings), {}}); - return result; - } - // [/materials] -} - namespace Tutorial { - Controls::Controls(CE::Runtime::Context& engine, const std::filesystem::path& assets) - : input_(engine.input()), - platform_(engine.platform_dispatcher().submission()), - uploader_(engine.resources()), - materials_(::materials(engine.resources(), assets)) {} + Controls::Controls(CE::Runtime::Context& engine) + : input_(engine.input()), platform_(engine.platform_dispatcher().submission()), uploader_(engine.resources()) {} // [widgets] void Controls::create_widgets(const CE::Game::TickContext& tick) { @@ -96,7 +62,7 @@ namespace Tutorial { static_cast(CE::UI::TGUI::adopt_scene(pending_, scene_)); if (!pending_.valid()) - pending_ = uploader_.submit(platform_, session_->record(), materials_); + pending_ = uploader_.submit(platform_, session_->record()); return applied; } // [/update] diff --git a/projects/apps/tutorial-game/src/controls.h b/projects/apps/tutorial-game/src/controls.h index b809b17d..1369a65e 100644 --- a/projects/apps/tutorial-game/src/controls.h +++ b/projects/apps/tutorial-game/src/controls.h @@ -6,7 +6,6 @@ #include -#include #include #include @@ -15,7 +14,6 @@ namespace Tutorial { CE::Input::System& input_; CE::Runtime::PlatformDispatcher::Submission platform_; CE::UI::TGUI::SceneUploader uploader_; - CE::UI::TGUI::Materials materials_; CE::UI::TGUI::Scene scene_; std::future pending_; std::unique_ptr session_; @@ -24,7 +22,7 @@ namespace Tutorial { bool ignore_quit_until_release_ = false; public: - Controls(CE::Runtime::Context& engine, const std::filesystem::path& assets); + explicit Controls(CE::Runtime::Context& engine); [[nodiscard]] bool permits_quit() const { return !ignore_quit_until_release_; } bool update(const CE::Game::TickContext& tick); void write(CE::Rendering::FrameWriter& frame) const; diff --git a/projects/apps/tutorial-game/src/game.cpp b/projects/apps/tutorial-game/src/game.cpp index 14c71904..cd0e0995 100644 --- a/projects/apps/tutorial-game/src/game.cpp +++ b/projects/apps/tutorial-game/src/game.cpp @@ -50,7 +50,7 @@ namespace Tutorial { selection.asset_directories = {asset_root_}; selection.automatic_system_fonts = false; hud_.emplace(engine_, CE::Text::FontCollection::load(selection), text_); - controls_.emplace(engine_, asset_root_); + controls_.emplace(engine_); } // [update] diff --git a/projects/engine/include/cheryl/assets/definitions/pipeline.h b/projects/engine/include/cheryl/assets/definitions/pipeline.h index 39b0a213..18228a3e 100644 --- a/projects/engine/include/cheryl/assets/definitions/pipeline.h +++ b/projects/engine/include/cheryl/assets/definitions/pipeline.h @@ -50,6 +50,11 @@ namespace CE::Rendering::Pipelines { } namespace CE::Rendering::Materials { + // Colored triangles with projection semantic "projection" and disabled depth/ + // culling. TexturedColor requires a custom Sampler2D named "image" and + // multiplies sampled RGBA by vertex RGBA without changing alpha convention. + enum class Builtin2D { SolidColor, TexturedColor }; + // Retains a pipeline generation; defaults own image handles/unit requests. // Material construction validates this partial custom layer, not a complete draw. struct Definition { diff --git a/projects/engine/include/cheryl/assets/resources/shader-asset-builder.h b/projects/engine/include/cheryl/assets/resources/shader-asset-builder.h index f5c3d771..9f10586b 100644 --- a/projects/engine/include/cheryl/assets/resources/shader-asset-builder.h +++ b/projects/engine/include/cheryl/assets/resources/shader-asset-builder.h @@ -18,5 +18,10 @@ namespace CE::Assets::Shaders { const ProgramRecipe& program, const std::shared_ptr& executable ) = 0; + // Uses backend-owned shader sources and bindings, without application + // files or catalogue publication. Construction obeys provider affinity; + // unsupported implementations reject rather than choosing another backend. + [[nodiscard]] virtual std::shared_ptr + build_builtin_material(CE::Rendering::Materials::Builtin2D kind, CE::Rendering::Pipelines::Blend blend); }; } diff --git a/projects/engine/src/assets/resources/shader-asset-builder.cpp b/projects/engine/src/assets/resources/shader-asset-builder.cpp new file mode 100644 index 00000000..73e13b09 --- /dev/null +++ b/projects/engine/src/assets/resources/shader-asset-builder.cpp @@ -0,0 +1,10 @@ +#include + +#include + +namespace CE::Assets::Shaders { + std::shared_ptr + Builder::build_builtin_material(CE::Rendering::Materials::Builtin2D, CE::Rendering::Pipelines::Blend) { + throw Exceptions::failed_operation(CE_HERE, "Resource provider does not support built-in 2D materials"); + } +} diff --git a/projects/engine/tests/include/testing/material-builder.h b/projects/engine/tests/include/testing/material-builder.h new file mode 100644 index 00000000..53f846be --- /dev/null +++ b/projects/engine/tests/include/testing/material-builder.h @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace CE::Testing { + // Records provider requests; tests supply resource handles independently. + struct MaterialBuilder final : CE::Assets::Shaders::Builder { + std::map> values; + std::vector> requests; + std::vector owners; + std::size_t fail_request = 0; + bool missing = false; + + void validate_program(const CE::Assets::Shaders::ProgramRecipe&) const override { + throw std::logic_error("Built-in requests must not require application shader recipes"); + } + void validate_material(const CE::Assets::Shaders::MaterialRecipe&, const CE::Assets::Shaders::ProgramRecipe&) const override { + throw std::logic_error("Built-in requests must not require application material recipes"); + } + std::shared_ptr build_program(const CE::Assets::Shaders::ProgramRecipe&) override { + throw std::logic_error("Built-in requests must not require application shader compilation"); + } + std::shared_ptr build_material( + const CE::Assets::Shaders::MaterialRecipe&, + const CE::Assets::Shaders::ProgramRecipe&, + const std::shared_ptr& + ) override { + throw std::logic_error("Built-in requests must not require application material construction"); + } + std::shared_ptr + build_builtin_material(const CE::Rendering::Materials::Builtin2D kind, const CE::Rendering::Pipelines::Blend blend) override { + requests.emplace_back(kind, blend); + owners.push_back(std::this_thread::get_id()); + if (requests.size() == fail_request) + throw std::runtime_error("Injected built-in material failure"); + return missing ? nullptr : values.at(kind); + } + }; +} diff --git a/projects/engine/tests/unit/src/shader-assets.cpp b/projects/engine/tests/unit/src/shader-assets.cpp index 30140f58..59e21d8f 100644 --- a/projects/engine/tests/unit/src/shader-assets.cpp +++ b/projects/engine/tests/unit/src/shader-assets.cpp @@ -204,6 +204,18 @@ namespace { }; } +TEST(shader_assets, unsupported_builtin) { + RecordingProvider provider; + EXPECT_THROW( + static_cast(provider.shader_asset_builder()->build_builtin_material( + CE::Rendering::Materials::Builtin2D::SolidColor, CE::Rendering::Pipelines::Blend::StraightAlpha + )), + CE::Exceptions::failed_operation + ); + EXPECT_EQ(provider.program_builds, 0u); + EXPECT_EQ(provider.material_builds, 0u); +} + TEST(shader_assets, selected_sources) { TemporaryShaders files; files.write("unlisted.json", "malformed JSON"); diff --git a/projects/modules/graphics/opengl/README.md b/projects/modules/graphics/opengl/README.md index 5cd30b52..8190f1f6 100644 --- a/projects/modules/graphics/opengl/README.md +++ b/projects/modules/graphics/opengl/README.md @@ -96,6 +96,12 @@ and private helpers live together under `src/backends/opengl/`. The provider also implements optional `CE::Assets::Shaders::Builder` construction for [indexed recipes](../../../../docs/assets/asset-manifests.md#shadermaterial-definitions), linking prepared owned source bytes and interpreting only `bindings.opengl`. +The same builder realizes Engine's `Builtin2D` solid/textured colored-triangle +profiles from embedded GLSL and bindings. TGUI and RmlUi request their own alpha +state through that generic contract, without linking this module. Programs are +weakly reused by profile and materials by profile/blend within one provider; +requests still require its live owner/current context on cache hits. See +[default UI materials](../../../../docs/rendering/pipelines-and-materials.md#default-ui-materials). Manual pipeline/material builders remain available. Engine image defaults request maximum-supported anisotropy; explicit samplers preserve independent immutable sampling for shared textures. See the diff --git a/projects/modules/graphics/opengl/include/cheryl/backends/opengl/resource-provider.h b/projects/modules/graphics/opengl/include/cheryl/backends/opengl/resource-provider.h index d9594830..21b2f171 100644 --- a/projects/modules/graphics/opengl/include/cheryl/backends/opengl/resource-provider.h +++ b/projects/modules/graphics/opengl/include/cheryl/backends/opengl/resource-provider.h @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace CE::Rendering::OpenGL { @@ -23,6 +24,11 @@ namespace CE::Rendering::OpenGL { private CE::Assets::Shaders::Builder { Renderer& renderer_; std::map> samplers_; + std::map> builtin_programs_; + std::map< + std::pair, + std::weak_ptr> + builtin_materials_; public: explicit ResourceProvider(Renderer& renderer) @@ -50,6 +56,8 @@ namespace CE::Rendering::OpenGL { build_material(CE::Rendering::Materials::Definition definition); private: + [[nodiscard]] CE_OPENGL_API std::shared_ptr + build_builtin_material(CE::Rendering::Materials::Builtin2D kind, CE::Rendering::Pipelines::Blend blend) override; CE_OPENGL_API void validate_program(const CE::Assets::Shaders::ProgramRecipe& recipe) const override; CE_OPENGL_API void validate_material( const CE::Assets::Shaders::MaterialRecipe& recipe, diff --git a/projects/modules/graphics/opengl/src/backends/opengl/builtin-materials.cpp b/projects/modules/graphics/opengl/src/backends/opengl/builtin-materials.cpp new file mode 100644 index 00000000..c3600f13 --- /dev/null +++ b/projects/modules/graphics/opengl/src/backends/opengl/builtin-materials.cpp @@ -0,0 +1,94 @@ +#include + +#include +#include + +#include +#include + +namespace CE::Rendering::OpenGL { + namespace { + constexpr auto vertex_source = R"GLSL(#version 330 core +uniform mat4 projectionMatrix; +layout(location = 0) in vec3 in_Position; +layout(location = 1) in vec2 in_Texcoord; +layout(location = 2) in vec4 in_Color; +out vec2 v_texcoord; +out vec4 v_color; +void main() { + gl_Position = projectionMatrix * vec4(in_Position, 1.0); + v_texcoord = in_Texcoord; + v_color = in_Color; +} +)GLSL"; + + constexpr auto solid_source = R"GLSL(#version 330 core +in vec4 v_color; +out vec4 out_Color; +void main() { + out_Color = v_color; +} +)GLSL"; + + constexpr auto textured_source = R"GLSL(#version 330 core +uniform sampler2D mytexture; +in vec2 v_texcoord; +in vec4 v_color; +out vec4 out_Color; +void main() { + out_Color = texture(mytexture, v_texcoord) * v_color; +} +)GLSL"; + } + + std::shared_ptr + ResourceProvider::build_builtin_material(const CE::Rendering::Materials::Builtin2D kind, const CE::Rendering::Pipelines::Blend blend) { + // A warm cache must still reject a stopped domain or the wrong owner. + static_cast(renderer_.resources()); + const auto key = std::pair{kind, blend}; + if (const auto cached = builtin_materials_.find(key); cached != builtin_materials_.end()) + if (auto material = cached->second.lock()) + return material; + + bool textured; + switch (kind) { + case CE::Rendering::Materials::Builtin2D::SolidColor: + textured = false; + break; + case CE::Rendering::Materials::Builtin2D::TexturedColor: + textured = true; + break; + default: + throw Exceptions::invalid_args(CE_HERE, "Unknown built-in 2D material"); + } + const std::string name = textured ? "textured-color-2d" : "solid-color-2d"; + const CE::Assets::Shaders::ProgramRecipe program_recipe{ + "cheryl.opengl:" + name, + {{CE::Assets::Shaders::Stage::Vertex, "builtin/colored-2d.vert", vertex_source}, + {CE::Assets::Shaders::Stage::Fragment, "builtin/" + name + ".frag", textured ? textured_source : solid_source}} + }; + CE::Assets::Shaders::MaterialRecipe recipe; + recipe.id = "cheryl.opengl:" + name + "-material"; + recipe.program = program_recipe.id; + recipe.vertex_layout = CE::Rendering::Geometry::Layout2D::Position3UV2Color4; + recipe.topology = CE::Rendering::Geometry::Topology::Triangles; + recipe.state = {blend, CE::Rendering::Pipelines::Depth::Disabled, false, CE::Rendering::Pipelines::Cull::None}; + recipe.parameters = {{"projection", CE::Rendering::Parameters::Type::Mat4, true, CE::Rendering::Parameters::Semantic::Projection}}; + if (textured) + recipe.parameters.push_back({"image", CE::Rendering::Parameters::Type::Sampler2D}); + recipe.bindings.emplace( + "opengl", textured ? R"JSON({"parameters":{"projection":"projectionMatrix","image":"mytexture"}})JSON" + : R"JSON({"parameters":{"projection":"projectionMatrix"}})JSON" + ); + validate_material(recipe, program_recipe); + auto& cached_program = builtin_programs_[kind]; + auto program = cached_program.lock(); + if (!program) { + program = build_program(program_recipe); + cached_program = program; + } + auto material = build_material(recipe, program_recipe, program); + builtin_materials_.insert_or_assign(key, material); + return material; + } +} diff --git a/projects/modules/graphics/opengl/tests/acceptance/src/native-opengl.cpp b/projects/modules/graphics/opengl/tests/acceptance/src/native-opengl.cpp index 43fd0d7a..64a5b1a4 100644 --- a/projects/modules/graphics/opengl/tests/acceptance/src/native-opengl.cpp +++ b/projects/modules/graphics/opengl/tests/acceptance/src/native-opengl.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -472,6 +474,107 @@ void main() { }; } +TEST(native_opengl, builtin_materials) { + if (!native_checks_requested()) + GTEST_SKIP() << "Set CHERYL_NATIVE_GL_TESTS=1 with a real GLFW display to run native acceptance"; + auto engine = CE::Application::GLFWOpenGL::make_context(small_window()); + auto& renderer = dynamic_cast(engine->renderer()); + auto& resources = engine->resources(); + renderer.initialize(); + auto* builder = resources.shader_asset_builder(); + ASSERT_NE(builder, nullptr); + using Kind = CE::Rendering::Materials::Builtin2D; + using Blend = CE::Rendering::Pipelines::Blend; + std::vector> retained; + std::array programs{}; + renderer.set_viewport({64, 64}); + renderer.set_clear_colour(0, 0, 0, 1); + for (const auto blend : {Blend::StraightAlpha, Blend::PremultipliedAlpha}) { + for (const auto kind : {Kind::SolidColor, Kind::TexturedColor}) { + const auto material = builder->build_builtin_material(kind, blend); + ASSERT_TRUE(material); + EXPECT_EQ(builder->build_builtin_material(kind, blend), material); + EXPECT_EQ(material->definition().pipeline->definition().state.blend, blend); + retained.push_back(material); + const bool textured = kind == Kind::TexturedColor; + const bool premultiplied = blend == Blend::PremultipliedAlpha; + const glm::vec4 color = textured ? (premultiplied ? glm::vec4{0.25f, 0.5f, 0.125f, 0.5f} : glm::vec4{0.5f, 1, 0.25f, 0.5f}) + : glm::vec4{premultiplied ? 0.5f : 1.0f, 0, 0, 0.5f}; + const std::array vertices{ + {{0, 0, 0, 0, 0, color.r, color.g, color.b, color.a}, + {128, 0, 0, 0, 0, color.r, color.g, color.b, color.a}, + {0, 128, 0, 0, 0, color.r, color.g, color.b, color.a}} + }; + const auto geometry = resources.upload_geometry(vertices, CE::Rendering::Geometry::Topology::Triangles); + CE::Rendering::DrawStyle2D style; + style.material = material; + if (textured) { + CE::Assets::Images::Decoded image{ + {1, 1}, premultiplied ? std::vector{64, 32, 128, 128} : std::vector{128, 64, 255, 128} + }; + style.parameters.emplace("image", CE::Rendering::Parameters::ImageBinding{resources.create_image(image), 0}); + } + CE::Rendering::Frame frame; + { + CE::Rendering::FrameWriter writer(frame); + auto pass = writer.begin_pass(glm::ortho(0.0f, 64.0f, 64.0f, 0.0f), glm::mat4{1}); + pass.add(CE::Rendering::resolve_draw_packet(geometry, 0, 3, style, pass.semantics(), {}, pass.constraints())); + } + renderer.clear(); + renderer.render(frame); + std::array pixel{}; + glReadBuffer(GL_BACK); + glReadPixels(32, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data()); + EXPECT_NEAR(pixel[0], textured ? 16 : 128, 1); + EXPECT_NEAR(pixel[1], textured ? 16 : 0, 1); + EXPECT_NEAR(pixel[2], textured ? 16 : 0, 1); + EXPECT_EQ(pixel[3], 255); + GLint program = 0; + glGetIntegerv(GL_CURRENT_PROGRAM, &program); + if (blend == Blend::StraightAlpha) + programs[textured ? 1 : 0] = static_cast(program); + else + EXPECT_EQ(static_cast(program), programs[textured ? 1 : 0]); + EXPECT_EQ(glGetError(), GL_NO_ERROR); + } + } + EXPECT_NE(programs[0], programs[1]); + EXPECT_THROW( + static_cast(builder->build_builtin_material(static_cast(99), Blend::StraightAlpha)), CE::Exceptions::invalid_args + ); + EXPECT_THROW( + static_cast(builder->build_builtin_material(Kind::SolidColor, static_cast(99))), CE::Exceptions::invalid_args + ); + std::exception_ptr foreign_error; + std::thread foreign([&] { + try { + static_cast(builder->build_builtin_material(Kind::SolidColor, Blend::StraightAlpha)); + } catch (...) { + foreign_error = std::current_exception(); + } + }); + foreign.join(); + ASSERT_TRUE(foreign_error); + EXPECT_THROW(std::rethrow_exception(foreign_error), CE::Exceptions::failed_operation); + { + CE::Rendering::OpenGL::ResourceProvider other(renderer); + EXPECT_NE(other.shader_asset_builder()->build_builtin_material(Kind::SolidColor, Blend::StraightAlpha), retained.front()); + } + const std::weak_ptr lifetime = retained.front(); + retained.clear(); + EXPECT_TRUE(lifetime.expired()); + glUseProgram(0); + renderer.maintain_resources(); + EXPECT_EQ(glIsProgram(programs[0]), GL_FALSE); + EXPECT_EQ(glIsProgram(programs[1]), GL_FALSE); + const auto warm = builder->build_builtin_material(Kind::SolidColor, Blend::StraightAlpha); + ASSERT_TRUE(warm); + renderer.deinitialize(); + EXPECT_THROW( + static_cast(builder->build_builtin_material(Kind::SolidColor, Blend::StraightAlpha)), CE::Exceptions::failed_operation + ); +} + TEST(native_opengl, ui_clipping_color) { if (!native_checks_requested()) GTEST_SKIP() << "Set CHERYL_NATIVE_GL_TESTS=1 with a real GLFW display to run native acceptance"; diff --git a/projects/modules/ui/rmlui/README.md b/projects/modules/ui/rmlui/README.md index c5582629..c627dedc 100644 --- a/projects/modules/ui/rmlui/README.md +++ b/projects/modules/ui/rmlui/README.md @@ -2,7 +2,7 @@ Use `Cheryl::UI::RmlUi` to author native RML documents and RCSS styles, receive Engine input and publish retained scenes. The application owns its session and -chooses the materials and pass order. RmlUi owns document layout and editing; +chooses the pass order. RmlUi owns document layout and editing; Engine does not acquire a common widget API or a toolkit dependency. ## Selection and dependency @@ -19,8 +19,8 @@ target_link_libraries(game PRIVATE Cheryl::UI::RmlUi) ## Create an editable document [Create an interactive GUI with RmlUi](../../../../docs/getting-started/how-to/create-gui.md) -provides the full First Game extension: module selection, font/shader assets, -RML and RCSS files, compatible premultiplied materials, native event listeners, +provides the full First Game extension: module selection, font assets, +RML and RCSS files, native event listeners, focus routing, retained scene publication and partial-safe cleanup. The [maintained RmlUi demo](../../../apps/demo/src/rmlui-demo.cpp) also @@ -93,13 +93,13 @@ Session texture bounds are at least 1024, matching the retained font atlas's maximum dimension; the default bound is 4096. File/generated images must fit the configured bound. Standalone CPU `RenderTarget` checks can use smaller bounds. -The application supplies `Materials` with colored triangle pipelines, -`PremultipliedAlpha`, disabled depth/write/culling, a projection semantic and a -custom image sampler for textured draws. Shaders multiply image RGBA by vertex -RGBA without an additional premultiplication. `SceneUploader` belongs to one -provider/domain and platform owner; `submit` transfers owned recording data through -Cheryl's dispatcher. `adopt_scene` never waits and replaces the retained scene only -after a complete successful upload. Failure or cancellation preserves its predecessor. +Construct `SceneUploader(provider)` on the platform owner during initialization. +`submit(platform_submission, recording)` transfers owned recording data through +Cheryl's dispatcher and selects automatic materials. `upload(provider, recording)` +uses the same defaults when already on that owner. The uploader belongs to one +provider/domain and platform owner. `adopt_scene` never waits and replaces the +retained scene only after a complete successful upload. Failure or cancellation +preserves its predecessor. ## Input identities @@ -108,15 +108,22 @@ keys remain `KI_UNKNOWN`. Committed characters come from `TextEvent`, not from this mapping. `modifiers` preserves Control, Shift, Alt, Super, Caps Lock and Num Lock flags in RmlUi's modifier representation. -## Material construction +## Default and custom materials + +The first nonempty upload creates defaults through the provider's generic shader +builder on the platform owner. OpenGL embeds the required shaders and reuses live +matching materials; applications need no UI shader files or native builder. +Unsupported providers reject the default request. -The demo retains manual pipeline/material construction on the graphics owner; -the adapter receives compatible premultiplied materials from its caller. The -[manual example](../../../../docs/rendering/pipelines-and-materials.md#manual-material-construction) -shows typed definitions and explicit uniform wiring. Indexed loading migrates -only main text/image materials. A later UI migration targets TGUI first; migrating -RmlUi also requires preserving a separately usable manual example. Core support -and planned effects remain independently scoped. +The optional third argument to `submit` or `upload` supplies explicit `Materials` +handles, including materials loaded from JSON. They require colored triangles, +`PremultipliedAlpha`, disabled depth/write/culling, a projection semantic and a +custom image sampler for textured draws. Shaders multiply image RGBA by vertex +RGBA without an additional premultiplication. Other custom values can come from +material defaults or `Materials::pass_parameters`. Passing `Materials{}` explicitly +selects an empty override; it does not request defaults for nonempty draws. +See [custom UI materials](../../../../docs/rendering/pipelines-and-materials.md#custom-ui-materials) +for handle selection and the separate manual construction API. ## Dependency configuration diff --git a/projects/modules/ui/rmlui/include/cheryl/ui/rmlui/scene.h b/projects/modules/ui/rmlui/include/cheryl/ui/rmlui/scene.h index 6f4bf02b..cd3b5446 100644 --- a/projects/modules/ui/rmlui/include/cheryl/ui/rmlui/scene.h +++ b/projects/modules/ui/rmlui/include/cheryl/ui/rmlui/scene.h @@ -45,9 +45,14 @@ namespace CE::UI::RmlUi { public: CE_UI_RMLUI_API explicit SceneUploader(const CE::Rendering::Resources::Provider& provider); + // Defaults are built lazily on the provider owner from backend-owned + // resources. Explicit Materials select application overrides instead. + [[nodiscard]] CE_UI_RMLUI_API Scene upload(CE::Rendering::Resources::Provider& provider, const RecordedScene& recording) const; [[nodiscard]] CE_UI_RMLUI_API Scene upload(CE::Rendering::Resources::Provider& provider, const RecordedScene& recording, const Materials& materials) const; [[nodiscard]] CE_UI_RMLUI_API std::future + submit(const CE::Runtime::PlatformDispatcher::Submission& platform, RecordedScene recording) const; + [[nodiscard]] CE_UI_RMLUI_API std::future submit(const CE::Runtime::PlatformDispatcher::Submission& platform, RecordedScene recording, Materials materials) const; }; diff --git a/projects/modules/ui/rmlui/src/scene.cpp b/projects/modules/ui/rmlui/src/scene.cpp index 21d0999a..e9c779f3 100644 --- a/projects/modules/ui/rmlui/src/scene.cpp +++ b/projects/modules/ui/rmlui/src/scene.cpp @@ -1,5 +1,7 @@ #include +#include + #include #include #include @@ -22,7 +24,7 @@ namespace CE::UI::RmlUi { void validate_material(const std::shared_ptr& material) { if (!material) - throw Exceptions::invalid_args(CE_HERE, "RmlUi draw needs an application-supplied material"); + throw Exceptions::invalid_args(CE_HERE, "RmlUi draw needs a compatible material"); const auto& pipeline = material->definition().pipeline->definition(); const auto& state = pipeline.state; if (pipeline.vertex_layout != CE::Rendering::Geometry::Layout2D::Position3UV2Color4 || @@ -67,6 +69,7 @@ namespace CE::UI::RmlUi { struct SceneUploader::State { const CE::Diagnostics::DomainId domain; const std::thread::id owner; + Materials defaults; std::map, std::weak_ptr, std::owner_less<>> images; @@ -77,6 +80,30 @@ namespace CE::UI::RmlUi { SceneUploader::SceneUploader(const CE::Rendering::Resources::Provider& provider) : state_(std::make_shared(provider)) {} + Scene SceneUploader::upload(CE::Rendering::Resources::Provider& provider, const RecordedScene& recording) const { + if (std::this_thread::get_id() != state_->owner || provider.diagnostic_id() != state_->domain) + throw Exceptions::failed_operation(CE_HERE, "RmlUi upload requires its original provider and platform owner"); + if (recording.draws().empty()) + return Scene{}; + if (!state_->defaults.solid || !state_->defaults.textured) { + auto* builder = provider.shader_asset_builder(); + if (!builder) + throw Exceptions::failed_operation(CE_HERE, "RmlUi defaults require a provider with built-in 2D materials"); + Materials candidate; + candidate.solid = builder->build_builtin_material( + CE::Rendering::Materials::Builtin2D::SolidColor, CE::Rendering::Pipelines::Blend::PremultipliedAlpha + ); + candidate.textured = builder->build_builtin_material( + CE::Rendering::Materials::Builtin2D::TexturedColor, CE::Rendering::Pipelines::Blend::PremultipliedAlpha + ); + if (!candidate.solid || !candidate.textured) + throw Exceptions::failed_operation(CE_HERE, "RmlUi provider returned missing built-in 2D materials"); + validate_materials(recording, candidate); + state_->defaults = std::move(candidate); + } + return upload(provider, recording, state_->defaults); + } + Scene SceneUploader::upload(CE::Rendering::Resources::Provider& provider, const RecordedScene& recording, const Materials& materials) const { if (std::this_thread::get_id() != state_->owner || provider.diagnostic_id() != state_->domain) @@ -122,6 +149,12 @@ namespace CE::UI::RmlUi { return scene; } + std::future SceneUploader::submit(const CE::Runtime::PlatformDispatcher::Submission& platform, RecordedScene recording) const { + return platform.submit([uploader = *this, recording = std::move(recording)](CE::Runtime::Context& engine) { + return uploader.upload(engine.resources(), recording); + }); + } + std::future SceneUploader::submit(const CE::Runtime::PlatformDispatcher::Submission& platform, RecordedScene recording, Materials materials) const { return platform.submit([uploader = *this, recording = std::move(recording), diff --git a/projects/modules/ui/rmlui/tests/unit/src/runtime.cpp b/projects/modules/ui/rmlui/tests/unit/src/runtime.cpp index 6d1e93c0..f47a2523 100644 --- a/projects/modules/ui/rmlui/tests/unit/src/runtime.cpp +++ b/projects/modules/ui/rmlui/tests/unit/src/runtime.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "../../support/document.h" #include @@ -131,12 +132,22 @@ namespace { void draw(std::size_t, std::size_t) const override {} }; + Materials materials(); + class Resources final : public CE::Rendering::Resources::Provider { RuntimeState& state_; + CE::Testing::MaterialBuilder builder_; public: explicit Resources(RuntimeState& state) - : state_(state) {} + : state_(state) { + const auto supplied = materials(); + builder_.values = { + {CE::Rendering::Materials::Builtin2D::SolidColor, supplied.solid}, + {CE::Rendering::Materials::Builtin2D::TexturedColor, supplied.textured} + }; + } + CE::Assets::Shaders::Builder* shader_asset_builder() noexcept override { return &builder_; } std::shared_ptr create_image(const CE::Assets::Images::Decoded& data) override { EXPECT_EQ(std::this_thread::get_id(), state_.platform); auto image = std::make_shared(data); @@ -264,7 +275,6 @@ namespace { CE::Runtime::Context& engine_; RuntimeState& state_; SceneUploader uploader_; - Materials materials_; std::unique_ptr session_; Rml::Element* picture_ = nullptr; Scene current_; @@ -274,14 +284,13 @@ namespace { public: Game(CE::Runtime::Context& engine, RuntimeState& state) : engine_(engine), state_(state), uploader_(engine.resources()) {} - void init() override { materials_ = materials(); } + void init() override {} void deinit() override { EXPECT_EQ(std::this_thread::get_id(), state_.platform); picture_ = nullptr; session_.reset(); // The runtime has joined simulation before this hook. current_ = {}; pending_ = {}; - materials_ = {}; state_.game_stopped = true; } void update(const CE::Game::TickContext& tick) override { @@ -310,12 +319,12 @@ namespace { auto recording = session_->record(); // Change the native image before the queued upload sees its old pixels. picture_->SetAttribute("src", std::filesystem::path{CHERYL_RMLUI_TEST_BLUE_IMAGE}.filename().generic_string()); - pending_ = uploader_.submit(engine_.platform_dispatcher().submission(), std::move(recording), materials_); + pending_ = uploader_.submit(engine_.platform_dispatcher().submission(), std::move(recording)); } else { EXPECT_EQ(std::this_thread::get_id(), state_.simulation); (void)adopt_scene(pending_, current_); if (state_.red_seen.load() && !blue_submitted_) { - pending_ = uploader_.submit(engine_.platform_dispatcher().submission(), session_->record(), materials_); + pending_ = uploader_.submit(engine_.platform_dispatcher().submission(), session_->record()); blue_submitted_ = true; } if (state_.blue_seen.load()) @@ -341,6 +350,11 @@ namespace { Game game(engine, state); CE::Runtime::GameRuntime runtime(engine, game, mode); runtime.run(); + const auto* builder = dynamic_cast(engine.resources().shader_asset_builder()); + ASSERT_NE(builder, nullptr); + EXPECT_EQ(builder->requests.size(), 2u); + for (const auto owner : builder->owners) + EXPECT_EQ(owner, state.platform); EXPECT_TRUE(state.red_seen.load()); EXPECT_TRUE(state.blue_seen.load()); EXPECT_TRUE(state.game_stopped); diff --git a/projects/modules/ui/rmlui/tests/unit/src/scene.cpp b/projects/modules/ui/rmlui/tests/unit/src/scene.cpp index 18e498f4..20cea142 100644 --- a/projects/modules/ui/rmlui/tests/unit/src/scene.cpp +++ b/projects/modules/ui/rmlui/tests/unit/src/scene.cpp @@ -2,10 +2,12 @@ #include #include +#include #include #include #include +#include #include #include @@ -36,6 +38,8 @@ namespace { }; struct MemoryProvider final : CE::Rendering::Resources::Provider { + CE::Assets::Shaders::Builder* builder = nullptr; + int image_uploads = 0; int geometry_uploads = 0; int fail_geometry = 0; @@ -44,6 +48,7 @@ namespace { std::vector> images; std::vector> geometry; + CE::Assets::Shaders::Builder* shader_asset_builder() noexcept override { return builder; } std::shared_ptr create_image(const CE::Assets::Images::Decoded& data) override { ++image_uploads; if (missing_image) @@ -84,7 +89,7 @@ namespace { : CE::Rendering::Resources::Pipeline(std::move(definition)) {} }; - Materials materials(const bool compatible = true) { + Materials materials(const bool compatible = true, const std::string& image_parameter = "pixels") { CE::Rendering::Pipelines::Definition definition; definition.program_sources = {"test-only-ui"}; definition.vertex_layout = @@ -99,11 +104,11 @@ namespace { result.solid = std::make_shared( CE::Rendering::Materials::Definition{std::make_shared(definition), {}} ); - definition.parameters.push_back({"pixels", CE::Rendering::Parameters::Type::Sampler2D}); + definition.parameters.push_back({image_parameter, CE::Rendering::Parameters::Type::Sampler2D}); result.textured = std::make_shared( CE::Rendering::Materials::Definition{std::make_shared(std::move(definition)), {}} ); - result.image_parameter = "pixels"; + result.image_parameter = image_parameter; result.image_unit = 3; return result; } @@ -331,3 +336,97 @@ TEST(ui_rmlui_scene, empty_scene) { EXPECT_EQ(provider.image_uploads, 0); EXPECT_EQ(provider.geometry_uploads, 0); } + +TEST(ui_rmlui_scene, defaults) { + const auto supplied = materials(true, "image"); + CE::Testing::MaterialBuilder builder; + builder.values = { + {CE::Rendering::Materials::Builtin2D::SolidColor, supplied.solid}, + {CE::Rendering::Materials::Builtin2D::TexturedColor, supplied.textured} + }; + MemoryProvider provider; + provider.builder = &builder; + SceneUploader uploader(provider); + EXPECT_TRUE(builder.requests.empty()); + const auto solid = uploader.upload(provider, recording()); + const auto textured = uploader.upload(provider, recording(texture())); + ASSERT_EQ(builder.requests.size(), 2u); + EXPECT_EQ( + builder.requests[0], + (std::pair{CE::Rendering::Materials::Builtin2D::SolidColor, CE::Rendering::Pipelines::Blend::PremultipliedAlpha}) + ); + EXPECT_EQ( + builder.requests[1], + (std::pair{CE::Rendering::Materials::Builtin2D::TexturedColor, CE::Rendering::Pipelines::Blend::PremultipliedAlpha}) + ); + ASSERT_EQ(solid.draws().size(), 1u); + ASSERT_EQ(textured.draws().size(), 1u); + EXPECT_EQ(solid.draws()[0].material, supplied.solid); + EXPECT_EQ(textured.draws()[0].material, supplied.textured); + EXPECT_EQ(std::get(textured.draws()[0].parameters.at("image")).unit, 0u); + const auto custom = materials(); + const auto overridden = uploader.upload(provider, recording(texture()), custom); + EXPECT_EQ(overridden.draws()[0].material, custom.textured); + EXPECT_EQ(std::get(overridden.draws()[0].parameters.at("pixels")).unit, 3u); + const auto restored = uploader.upload(provider, recording()); + EXPECT_EQ(restored.draws()[0].material, supplied.solid); + EXPECT_EQ(builder.requests.size(), 2u); +} + +TEST(ui_rmlui_scene, unsupported_defaults) { + MemoryProvider provider; + SceneUploader uploader(provider); + EXPECT_THROW(static_cast(uploader.upload(provider, recording())), CE::Exceptions::failed_operation); + EXPECT_TRUE(uploader.upload(provider, RecordedScene{}).draws().empty()); + EXPECT_NO_THROW(static_cast(uploader.upload(provider, recording(), materials()))); +} + +TEST(ui_rmlui_scene, failed_defaults) { + for (const bool missing : {false, true}) { + SCOPED_TRACE(missing); + const auto supplied = materials(true, "image"); + CE::Testing::MaterialBuilder builder; + builder.values = { + {CE::Rendering::Materials::Builtin2D::SolidColor, supplied.solid}, + {CE::Rendering::Materials::Builtin2D::TexturedColor, supplied.textured} + }; + builder.missing = missing; + builder.fail_request = missing ? 0 : 2; + MemoryProvider provider; + provider.builder = &builder; + SceneUploader uploader(provider); + if (missing) + EXPECT_THROW(static_cast(uploader.upload(provider, recording())), CE::Exceptions::failed_operation); + else + EXPECT_THROW(static_cast(uploader.upload(provider, recording())), std::runtime_error); + EXPECT_EQ(provider.geometry_uploads, 0); + builder.fail_request = 0; + builder.missing = false; + const auto scene = uploader.upload(provider, recording()); + EXPECT_EQ(scene.draws().size(), 1u); + EXPECT_EQ(builder.requests.size(), 4u); + } +} + +TEST(ui_rmlui_scene, default_owner) { + CE::Testing::MaterialBuilder builder; + MemoryProvider provider; + provider.builder = &builder; + MemoryProvider other; + other.builder = &builder; + SceneUploader uploader(provider); + const auto recorded = recording(); + EXPECT_THROW(static_cast(uploader.upload(other, recorded)), CE::Exceptions::failed_operation); + std::exception_ptr error; + std::thread foreign([&] { + try { + static_cast(uploader.upload(provider, recorded)); + } catch (...) { + error = std::current_exception(); + } + }); + foreign.join(); + ASSERT_TRUE(error); + EXPECT_THROW(std::rethrow_exception(error), CE::Exceptions::failed_operation); + EXPECT_TRUE(builder.requests.empty()); +} diff --git a/projects/modules/ui/tgui/README.md b/projects/modules/ui/tgui/README.md index fb871bb1..8932dfe6 100644 --- a/projects/modules/ui/tgui/README.md +++ b/projects/modules/ui/tgui/README.md @@ -3,8 +3,8 @@ `Cheryl::UI::TGUI` is an optional UI owner. It translates selected Cheryl input records and records/uploads toolkit draws as retained Cheryl scenes. A `Session` owns the custom backend, GUI, Engine-backed fonts and input leases. The -[selected demo](../../../apps/demo/README.md) supplies a representative panel and -application materials. +[selected demo](../../../apps/demo/README.md) supplies a representative panel using +automatic rendering defaults. The module uses a shared TGUI 1.13.0 runtime configured with the custom backend. Its Cheryl adapter supplies Engine font rasterization and image decoding, so TGUI @@ -44,12 +44,12 @@ before selecting the module. ## Use it in a game [Create a HUD with TGUI](../../../../docs/getting-started/create-a/hud.md) -provides the complete score-label/health-bar workflow, including materials, -layout and game-state updates. The +provides the complete score-label/health-bar workflow, including layout, +game-state updates and scene publication. The [maintained TGUI demo](../../../apps/demo/src/tgui-demo.cpp) also supplies interactive buttons, editing and application focus controls. The lifecycle is: -1. During platform-owned `init()`, construct application materials and a scene uploader. +1. During platform-owned `init()`, construct a scene uploader from the resource provider. 2. On the first simulation update, create a session and native widgets. 3. Supply copied sizes, simulation time and the complete input batch each update. 4. Record CPU scene data and keep one upload pending while retaining the previous scene. @@ -227,15 +227,24 @@ clearing remains with the application; TGUI's `mainLoop`/clear functions are not part of this bridge. Construct `SceneUploader(provider)` on the platform owner during initialization, -then copy its handle to simulation. The application supplies `Materials` with -colored-triangle pipelines, straight alpha, disabled depth/write/culling and -Cheryl's projection semantic. A textured material declares the named custom -`Sampler2D`; its shader multiplies sampled RGBA by vertex RGBA. Other custom values -come from material defaults or a copied pass layer accepted by each used pipeline. -Backend-specific pipeline construction stays in the application/graphics module. - -`submit(platform_submission, recording, materials)` transfers owned data through -the existing dispatcher and returns a future. Uploads require the original provider +then copy its handle to simulation. Use `submit(platform_submission, recording)` +for automatic materials, or `upload(provider, recording)` when already on the +platform owner. Defaults are created lazily for the first nonempty recording +through the provider's generic shader builder. OpenGL embeds their shaders and +reuses live matching materials; the application needs no UI shader files or +native graphics builder. An unsupported provider rejects the default request. + +The optional third argument supplies explicit `Materials` handles, including +materials loaded from [JSON recipes](../../../../docs/rendering/pipelines-and-materials.md#custom-ui-materials). +Those overrides require colored triangles, straight alpha, disabled +depth/write/culling and Cheryl's projection semantic. A textured material +declares the named custom `Sampler2D`; its shader multiplies sampled RGBA by +vertex RGBA. Other custom values come from material defaults or a copied pass +layer accepted by each used pipeline. Passing `Materials{}` explicitly selects +an empty override; it does not request defaults for nonempty draws. + +`submit` transfers owned data through the existing dispatcher and returns a +future. Uploads require the original provider domain and platform thread. The uploader reuses a live uploaded image for the same CPU generation; its weak cache does not retain abandoned CPU images or GPU handles. Changed pixels always produce another image. Geometry remains separately uploaded diff --git a/projects/modules/ui/tgui/include/cheryl/ui/tgui/scene.h b/projects/modules/ui/tgui/include/cheryl/ui/tgui/scene.h index 7914792a..3454c19a 100644 --- a/projects/modules/ui/tgui/include/cheryl/ui/tgui/scene.h +++ b/projects/modules/ui/tgui/include/cheryl/ui/tgui/scene.h @@ -50,9 +50,14 @@ namespace CE::UI::TGUI { public: CE_UI_TGUI_API explicit SceneUploader(const CE::Rendering::Resources::Provider& provider); + // Defaults are built lazily on the provider owner from backend-owned + // resources. Explicit Materials select application overrides instead. + [[nodiscard]] CE_UI_TGUI_API Scene upload(CE::Rendering::Resources::Provider& provider, const RecordedScene& recording) const; [[nodiscard]] CE_UI_TGUI_API Scene upload(CE::Rendering::Resources::Provider& provider, const RecordedScene& recording, const Materials& materials) const; [[nodiscard]] CE_UI_TGUI_API std::future + submit(const CE::Runtime::PlatformDispatcher::Submission& platform, RecordedScene recording) const; + [[nodiscard]] CE_UI_TGUI_API std::future submit(const CE::Runtime::PlatformDispatcher::Submission& platform, RecordedScene recording, Materials materials) const; }; diff --git a/projects/modules/ui/tgui/src/scene.cpp b/projects/modules/ui/tgui/src/scene.cpp index 01ad5cf5..afa173fa 100644 --- a/projects/modules/ui/tgui/src/scene.cpp +++ b/projects/modules/ui/tgui/src/scene.cpp @@ -1,5 +1,7 @@ #include +#include + #include "dependency-contract.h" #include @@ -25,7 +27,7 @@ namespace CE::UI::TGUI { void validate_material(const std::shared_ptr& material) { if (!material) - throw Exceptions::invalid_args(CE_HERE, "TGUI draw needs an application-supplied material"); + throw Exceptions::invalid_args(CE_HERE, "TGUI draw needs a compatible material"); const auto& pipeline = material->definition().pipeline->definition(); const auto& state = pipeline.state; if (pipeline.vertex_layout != CE::Rendering::Geometry::Layout2D::Position3UV2Color4 || @@ -69,6 +71,7 @@ namespace CE::UI::TGUI { struct SceneUploader::State { const CE::Diagnostics::DomainId domain; const std::thread::id owner; + Materials defaults; std::map, std::weak_ptr, std::owner_less<>> images; std::array, 2> samplers; @@ -80,6 +83,30 @@ namespace CE::UI::TGUI { SceneUploader::SceneUploader(const CE::Rendering::Resources::Provider& provider) : state_(std::make_shared(provider)) {} + Scene SceneUploader::upload(CE::Rendering::Resources::Provider& provider, const RecordedScene& recording) const { + if (std::this_thread::get_id() != state_->owner || provider.diagnostic_id() != state_->domain) + throw Exceptions::failed_operation(CE_HERE, "TGUI upload requires its original provider and platform owner"); + if (recording.draws().empty()) + return Scene{}; + if (!state_->defaults.solid || !state_->defaults.textured) { + auto* builder = provider.shader_asset_builder(); + if (!builder) + throw Exceptions::failed_operation(CE_HERE, "TGUI defaults require a provider with built-in 2D materials"); + Materials candidate; + candidate.solid = builder->build_builtin_material( + CE::Rendering::Materials::Builtin2D::SolidColor, CE::Rendering::Pipelines::Blend::StraightAlpha + ); + candidate.textured = builder->build_builtin_material( + CE::Rendering::Materials::Builtin2D::TexturedColor, CE::Rendering::Pipelines::Blend::StraightAlpha + ); + if (!candidate.solid || !candidate.textured) + throw Exceptions::failed_operation(CE_HERE, "TGUI provider returned missing built-in 2D materials"); + validate_materials(recording, candidate); + state_->defaults = std::move(candidate); + } + return upload(provider, recording, state_->defaults); + } + Scene SceneUploader::upload(CE::Rendering::Resources::Provider& provider, const RecordedScene& recording, const Materials& materials) const { if (std::this_thread::get_id() != state_->owner || provider.diagnostic_id() != state_->domain) @@ -139,6 +166,12 @@ namespace CE::UI::TGUI { return scene; } + std::future SceneUploader::submit(const CE::Runtime::PlatformDispatcher::Submission& platform, RecordedScene recording) const { + return platform.submit([uploader = *this, recording = std::move(recording)](CE::Runtime::Context& engine) { + return uploader.upload(engine.resources(), recording); + }); + } + std::future SceneUploader::submit(const CE::Runtime::PlatformDispatcher::Submission& platform, RecordedScene recording, Materials materials) const { return platform.submit([uploader = *this, recording = std::move(recording), diff --git a/projects/modules/ui/tgui/tests/unit/src/runtime.cpp b/projects/modules/ui/tgui/tests/unit/src/runtime.cpp index e62d7a8b..5e8d1f73 100644 --- a/projects/modules/ui/tgui/tests/unit/src/runtime.cpp +++ b/projects/modules/ui/tgui/tests/unit/src/runtime.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -136,12 +137,22 @@ namespace { void bind(std::uint32_t) const override {} }; + Materials materials(); + class Resources final : public CE::Rendering::Resources::Provider { RuntimeState& state_; + CE::Testing::MaterialBuilder builder_; public: explicit Resources(RuntimeState& state) - : state_(state) {} + : state_(state) { + const auto supplied = materials(); + builder_.values = { + {CE::Rendering::Materials::Builtin2D::SolidColor, supplied.solid}, + {CE::Rendering::Materials::Builtin2D::TexturedColor, supplied.textured} + }; + } + CE::Assets::Shaders::Builder* shader_asset_builder() noexcept override { return &builder_; } std::shared_ptr create_image(const CE::Assets::Images::Decoded& data) override { EXPECT_EQ(std::this_thread::get_id(), state_.platform); auto image = std::make_shared(data); @@ -282,7 +293,6 @@ namespace { CE::Runtime::Context& engine_; RuntimeState& state_; SceneUploader uploader_; - Materials materials_; std::unique_ptr session_; tgui::Picture::Ptr picture_; Scene current_; @@ -294,14 +304,13 @@ namespace { Game(CE::Runtime::Context& engine, RuntimeState& state) : engine_(engine), state_(state), uploader_(engine.resources()) {} - void init() override { materials_ = materials(); } + void init() override {} void deinit() override { EXPECT_EQ(std::this_thread::get_id(), state_.platform); picture_.reset(); session_.reset(); // The runtime has joined simulation before this hook. current_ = {}; pending_ = {}; - materials_ = {}; state_.game_stopped = true; } void update(const CE::Game::TickContext& tick) override { @@ -320,12 +329,12 @@ namespace { auto recording = session_->record(); // Mutate the widget before the queued upload sees its old recording. picture_->getRenderer()->setTexture(texture(true)); - pending_ = uploader_.submit(engine_.platform_dispatcher().submission(), std::move(recording), materials_); + pending_ = uploader_.submit(engine_.platform_dispatcher().submission(), std::move(recording)); } else { EXPECT_EQ(std::this_thread::get_id(), state_.simulation); (void)adopt_scene(pending_, current_); if (state_.red_seen.load() && !blue_submitted_) { - pending_ = uploader_.submit(engine_.platform_dispatcher().submission(), session_->record(), materials_); + pending_ = uploader_.submit(engine_.platform_dispatcher().submission(), session_->record()); blue_submitted_ = true; } if (state_.blue_seen.load()) @@ -352,6 +361,11 @@ namespace { CE::Runtime::GameRuntime runtime(engine, game, mode); game.runtime = &runtime; runtime.run(); + const auto* builder = dynamic_cast(engine.resources().shader_asset_builder()); + ASSERT_NE(builder, nullptr); + EXPECT_EQ(builder->requests.size(), 2u); + for (const auto owner : builder->owners) + EXPECT_EQ(owner, state.platform); EXPECT_TRUE(state.red_seen.load()); EXPECT_TRUE(state.blue_seen.load()); EXPECT_TRUE(state.game_stopped); diff --git a/projects/modules/ui/tgui/tests/unit/src/scene.cpp b/projects/modules/ui/tgui/tests/unit/src/scene.cpp index 358ad4aa..870f10cc 100644 --- a/projects/modules/ui/tgui/tests/unit/src/scene.cpp +++ b/projects/modules/ui/tgui/tests/unit/src/scene.cpp @@ -2,10 +2,12 @@ #include #include +#include #include #include #include +#include #include #include @@ -42,6 +44,8 @@ namespace { }; struct MemoryProvider final : CE::Rendering::Resources::Provider { + CE::Assets::Shaders::Builder* builder = nullptr; + int image_uploads = 0; int sampler_uploads = 0; int geometry_uploads = 0; @@ -55,6 +59,7 @@ namespace { std::vector> samplers; std::vector> geometry; + CE::Assets::Shaders::Builder* shader_asset_builder() noexcept override { return builder; } std::shared_ptr create_image(const CE::Assets::Images::Decoded& data) override { ++image_uploads; if (missing_image) @@ -108,7 +113,7 @@ namespace { : CE::Rendering::Resources::Pipeline(std::move(definition)) {} }; - Materials materials(const bool compatible = true) { + Materials materials(const bool compatible = true, const std::string& image_parameter = "pixels") { CE::Rendering::Pipelines::Definition definition; definition.program_sources = {"test-only-ui"}; definition.vertex_layout = @@ -122,11 +127,11 @@ namespace { result.solid = std::make_shared( CE::Rendering::Materials::Definition{std::make_shared(definition), {}} ); - definition.parameters.push_back({"pixels", CE::Rendering::Parameters::Type::Sampler2D}); + definition.parameters.push_back({image_parameter, CE::Rendering::Parameters::Type::Sampler2D}); result.textured = std::make_shared( CE::Rendering::Materials::Definition{std::make_shared(std::move(definition)), {}} ); - result.image_parameter = "pixels"; + result.image_parameter = image_parameter; result.image_unit = 3; return result; } @@ -427,3 +432,95 @@ TEST(ui_tgui_scene, empty_scene) { EXPECT_EQ(provider.image_uploads, 0); EXPECT_EQ(provider.geometry_uploads, 0); } + +TEST(ui_tgui_scene, defaults) { + const auto supplied = materials(true, "image"); + CE::Testing::MaterialBuilder builder; + builder.values = { + {CE::Rendering::Materials::Builtin2D::SolidColor, supplied.solid}, + {CE::Rendering::Materials::Builtin2D::TexturedColor, supplied.textured} + }; + MemoryProvider provider; + provider.builder = &builder; + SceneUploader uploader(provider); + EXPECT_TRUE(builder.requests.empty()); + const auto solid = uploader.upload(provider, recording()); + const auto textured = uploader.upload(provider, recording(texture())); + ASSERT_EQ(builder.requests.size(), 2u); + EXPECT_EQ( + builder.requests[0], (std::pair{CE::Rendering::Materials::Builtin2D::SolidColor, CE::Rendering::Pipelines::Blend::StraightAlpha}) + ); + EXPECT_EQ( + builder.requests[1], (std::pair{CE::Rendering::Materials::Builtin2D::TexturedColor, CE::Rendering::Pipelines::Blend::StraightAlpha}) + ); + ASSERT_EQ(solid.draws().size(), 1u); + ASSERT_EQ(textured.draws().size(), 1u); + EXPECT_EQ(solid.draws()[0].material, supplied.solid); + EXPECT_EQ(textured.draws()[0].material, supplied.textured); + EXPECT_EQ(std::get(textured.draws()[0].parameters.at("image")).unit, 0u); + const auto custom = materials(); + const auto overridden = uploader.upload(provider, recording(texture()), custom); + EXPECT_EQ(overridden.draws()[0].material, custom.textured); + EXPECT_EQ(std::get(overridden.draws()[0].parameters.at("pixels")).unit, 3u); + const auto restored = uploader.upload(provider, recording()); + EXPECT_EQ(restored.draws()[0].material, supplied.solid); + EXPECT_EQ(builder.requests.size(), 2u); +} + +TEST(ui_tgui_scene, unsupported_defaults) { + MemoryProvider provider; + SceneUploader uploader(provider); + EXPECT_THROW(static_cast(uploader.upload(provider, recording())), CE::Exceptions::failed_operation); + EXPECT_TRUE(uploader.upload(provider, RecordedScene{}).draws().empty()); + EXPECT_NO_THROW(static_cast(uploader.upload(provider, recording(), materials()))); +} + +TEST(ui_tgui_scene, failed_defaults) { + for (const bool missing : {false, true}) { + SCOPED_TRACE(missing); + const auto supplied = materials(true, "image"); + CE::Testing::MaterialBuilder builder; + builder.values = { + {CE::Rendering::Materials::Builtin2D::SolidColor, supplied.solid}, + {CE::Rendering::Materials::Builtin2D::TexturedColor, supplied.textured} + }; + builder.missing = missing; + builder.fail_request = missing ? 0 : 2; + MemoryProvider provider; + provider.builder = &builder; + SceneUploader uploader(provider); + if (missing) + EXPECT_THROW(static_cast(uploader.upload(provider, recording())), CE::Exceptions::failed_operation); + else + EXPECT_THROW(static_cast(uploader.upload(provider, recording())), std::runtime_error); + EXPECT_EQ(provider.geometry_uploads, 0); + builder.fail_request = 0; + builder.missing = false; + const auto scene = uploader.upload(provider, recording()); + EXPECT_EQ(scene.draws().size(), 1u); + EXPECT_EQ(builder.requests.size(), 4u); + } +} + +TEST(ui_tgui_scene, default_owner) { + CE::Testing::MaterialBuilder builder; + MemoryProvider provider; + provider.builder = &builder; + MemoryProvider other; + other.builder = &builder; + SceneUploader uploader(provider); + const auto recorded = recording(); + EXPECT_THROW(static_cast(uploader.upload(other, recorded)), CE::Exceptions::failed_operation); + std::exception_ptr error; + std::thread foreign([&] { + try { + static_cast(uploader.upload(provider, recorded)); + } catch (...) { + error = std::current_exception(); + } + }); + foreign.join(); + ASSERT_TRUE(error); + EXPECT_THROW(std::rethrow_exception(error), CE::Exceptions::failed_operation); + EXPECT_TRUE(builder.requests.empty()); +} From ca52b81c13e094122e0c96376e822d08de45ea54 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Sun, 18 Oct 2026 12:36:19 -0700 Subject: [PATCH 48/98] Merges completed engine and documentation branches into fixes-develop (squash) Consolidates the completed linear stack through docs/agent-workflows, including the user-authored .gitignore update. Preserves the original source branches and each distinct integrated commit message below. The aggregate tree matches the reviewed source tip; no content is changed by consolidation. Integrated original commits (oldest first): Original commit d16d634f9145cd39b838fc969a34e50bdb2a2edd Adds context-owned font selection and persistent font references Provides a shared CPU font catalogue with named families, application defaults, ordered glyph fallbacks and selectable groups. Returns immutable Font handles usable after catalogue teardown, with persistent application references for saves. Adds explicit random choice and restore-or-reselect operations with engine-owned or supplied randomness. Retains saved choices while they remain available group members; otherwise selects a replacement or the configured safety defaults. Preserves style variants, collection face indices and the existing FontSelection workflow, and exposes retained faces to GUI adapters without native library types. Adds Font layout/rasterizer overloads and focused persistence, style, ownership, concurrency, consumer and standalone-header coverage. Original commit 92e2c40641d186f27ee18bf48fea62984ac38857 Adds default materials to ordinary image and text assets Extends provider-owned built-in materials with ordinary textured triangle and strip profiles using projection, view, model, scale and alpha semantics. Shares compatible programs and preserves image sampling, including grayscale font atlas coverage and explicit premultiplied-alpha fades. Retains defaults during sprite, tileset, whole-image, Unicode text and AsciiFont uploads. Keeps explicit material composition and custom shader selection, and inserts the standard image binding during CPU draw resolution by default. Adds upload, override, failure, retained-submission and native rendering coverage. Preserves the deprecated bitmap loader's existing tuple and explicit-material API. Original commit 6cf93796cd3a17571772639bf5b86dc9b7a47ecc Integrates engine font defaults with TGUI and RmlUi sessions Adds context-aware and injected-catalogue sessions while preserving standalone input-based construction. Imports retained Engine fonts and defaults directly, without reopening source files or depending on a global Engine context. Bridges resolved Font handles to native widget/document authoring, retaining face indices, available styles and common glyph fallbacks. Preserves aliases through native teardown, isolates explicit and private registrations, and keeps partial registration identity tied to the original immutable snapshot. Covers defaults and inheritance, real style selection, collection indices, source/service lifetime, alias collisions, owner checks and standalone overload compatibility. Exercises the new paths in exported consumer applications. Original commit 0e7f59dd19bb70cdaba763f45548e8323966a2b1 Allows named defaults for ordered font selections Lets applications assign a persistent catalogue reference when configuring an existing ordered FontSelection. Preserves the application-default convenience overload and validates reserved names and source-reference collisions before publishing a replacement catalogue. Supports existing native document family names without adapter-owned loading or application font-path extraction. Adds transactional naming coverage and verifies that an unconfigured Runtime Context supplies its embedded font. Original commit 0f0ec5b010c760420afcfe64ed6d260aed90ae2a Simplifies game examples around engine material and font defaults Uses retained provider materials for First Game and the teaching game's sprites and direct text, removing their duplicate shader programs and material manifests. Keeps nearest pixel-art sampling as an explicit material configuration over the shared default pipeline and preserves per-object material composition. Uses the context font default for direct labels and TGUI controls. Configures the demo's existing ordered font options once under its authored document family name, then shares the same retained selection with Engine text and both toolkits. Removes source-tree font fallback paths and application-owned RmlUi font loading. Original commit cca1bb6ad6b9fa0b01b9c7146b7b37e32455a61c Updates guides for shared font selection and ordinary material defaults Teaches provider-owned sprite, image and text materials in First Game and the focused drawing tasks while preserving explicit material composition and pixel-art sampling choices. Removes obsolete shader asset prerequisites. Documents context-owned font configuration, retained handles, named families, ordered fallbacks, random groups and saved-reference restoration. Updates TGUI HUD and RmlUi GUI examples to inherit Engine defaults without application font registration, retaining native per-element styling and standalone escape paths. Reconciles owner guides and the active Unicode checklist, distinguishing tested registered style/index selection from remaining automatic-weight boundaries and native visual appearance limits. Original commit 09f1eeb2191652fa67712d4172e7b721837a7416 Adds independent input action contexts with scoped attachment Provides reusable local action definitions, producer-safe attachment endpoints and move-only context leases. Installs and removes mappings at input polling boundaries while preserving direct bindings and immutable tick views. Reuses existing binding evaluation and complete snapshot publication for chords, axes, focus gating and departure state. Adds focused coverage for isolation, cross-thread lifetime, cancellation, focus transitions and publication failure, plus public header probes. Original commit cf52a6043b05ab3b69971e264e18ea5ad8657a60 Adds retained asynchronous text updates and explicit retry Introduces a value-owned text request and updater that prepares on context-tracked workers, uploads through the platform dispatcher and adopts complete generations without blocking simulation. Coalesces desired content, preserves the last successful result after failure and exposes failed stages and revisions for application reporting and retry. Migrates the demo from application-owned worker, future and adoption bookkeeping. Covers request fields, font snapshots, coalescing, preparation and upload failures, retained packets, cancellation and shutdown with focused runtime tests and public header probes. Original commit 9c73ee1739af740567666d237fdad334e92b02cd Composes tutorial characters from controllers and a TGUI HUD Replaces overlapping Player and Enemy classes with one Character driven by a game-owned command. Demonstrates human input contexts, autonomous patrol behavior and runtime attachment/removal of a second keyboard player. Uses the maintained TGUI score and health HUD as the executable interface, replacing the previous direct-text HUD and separate controls panel. Keeps the First Game direct input/text path and adds focused gameplay regression coverage. Original commit 1816d24f3ebd65b2db72451a6ca9443a30059d9b Reorganizes tutorials and expands documentation task discovery Places Engine Systems last under Getting Started and makes How to a grouped SDK task index linking authoritative guides and maintained examples. Replaces duplicated Player and Enemy tutorials with Controller, Character and source-backed TGUI HUD steps. Documents independent input contexts and the retained text updater, including initialization, simulation, failure, retry and cleanup boundaries. Adds whole-image drawing and per-object material examples, repairs default-material/font entry routes and synchronizes affected setup, system, asset and application guides. Original commit 4b3df1a098c5be3c7f8eee59195b8e26e93d825e Fixes task index navigation and shortens task labels Moves the hidden walkthrough tree to page scope so guides no longer appear beneath the logging category. Shortens all task labels while retaining the complete task list and guide destinations. Adds a focused regression check for walkthrough ownership in the task index. Original commit 7692c81672f53023e964aae7a8734aee2a0ef5a8 Reorganizes documentation around game and engine development Combines onboarding and subject guides under Game Development, groups the tutorial sequence, and makes Task index a comprehensive lookup page. Gives procedures and concrete module references one topic owner and groups engine extension, validation and documentation tooling under Engine Development. Consolidates sprite and changing-text teaching around First Game and the retained updater, reconciles application assembly and SDK instructions, and relocates maintainer consumer checks. Preserves published paths and prior anchors while retaining unlisted notices for retired architecture overviews. Adds navigation ownership, sequence and scope regressions and synchronizes discovery links and source-owned reference titles. Original commit cfe84f9b41a97b5291cbcf3610a1144244986a45 Fixes incremental documentation navigation refresh Tracks Sphinx's parsed navigation and rewrites shared HTML when titles, captions, parentage or TOC depth changes. Retains parsed API trees and unchanged outputs for prose-only updates, with regression coverage for cache persistence and navigation mutations. Collapses inactive sidebar branches, keeps page hierarchy separate from section contents, and documents navigation invalidation and revision-only reuse. Original commit 1e07c0633cca46b519461edd4fc037917e614a35 Fixes sidebar controls for inactive navigation branches Preserves inactive branch children in the generated HTML so the theme can display disclosure buttons before a page is visited. The existing theme styles keep those branches collapsed until expanded. Updates the focused configuration regression to require the child markup needed by the controls. Original commit 44ed9cc85852c0779397e4820ed7597f0338a781 Fixes theme persistence between local documentation pages Carries validated light, dark or system preferences in local documentation links and GET search forms, including late search results and links opened in another tab. Updates the current file URL when the choice changes so reload retains the selection. Restricts propagation to HTML pages within the generated documentation root and preserves hosted storage behavior, other query values, anchors and browser history state. Adds dependency-free JavaScript regressions for local navigation, storage failures, search forms, system appearance and hosted pages. Original commit 68f4cbe8c384d88dcc9879eb424bb2c1daeed788 Adds documentation usefulness audit and per-page scores Defines seven weighted dimensions covering beginner entry, practical progress, expert contracts, source alignment, retrieval and use of the documentation hierarchy. Records review methods, confidence limits and baseline evidence. Scores 107 canonical authored units, 607 declaration articles and 15 meaningful header or migration entries. Inventories inherited wrappers and mechanical-page exclusions separately, with individual strengths, gaps and recommendations. Analyzes strong examples, factual drift, interrupted learning paths, unpublished API contracts and observed declaration-rendering defects. Provides an ordered improvement program and proposed reader measurements without claiming runtime acceptance or observed usability results. Original commit 6acd07054ed78c54e4af5cbb9c9b4d6952bdf559 Fixes documentation usefulness audit publication scope Makes the generated site the primary corpus: 91 manual pages, 607 declaration articles and 15 meaningful header or migration entries. Recomputes the manual summary and limits primary findings and recommended work to published content. Adds a published-only score export with rendered page paths and canonical content owners. Moves the 16 repository-only reviews into a separate appendix and records explicit primary and supplementary summaries without changing individual ratings or the original authored spreadsheet. Aligns the FontSource narrative score with its existing detailed review. Original commit ea8b9c0c18eebd76ab2370ce4764d8b6f696151d Adds focused documentation authoring guidance for agents Introduces a small routing entry and adaptable page outlines, clear-writing guidance, source-supported precision checks, audit scope rules and beginner/expert reader walkthroughs. Connects the guidance from repository and documentation instructions while retaining the existing workflow and authorization authority. Excludes repository-only agent guidance from the published site. Original commit 8b11fb55f432588cc11bc1a45423c59a8b8de5ba Centralizes published documentation and generation under docs/site Separates the published manual and its generator from repository-only status, planning, acceptance and agent authoring guidance. Preserves site docnames and navigation while updating CLI, CMake, CI, Read the Docs and edit-source paths. Makes module references canonical regular files in docs/site/modules and leaves relative README symlinks beside module code. Removes the superseded README projection registry and uses ordinary document links. Repairs inbound links, source comments and include paths; retains historical audit paths with an explicit relocation note. Adds focused layout, canonical-source and repository-link regressions. Protects all authored docs from output overrides and preserves legacy HTML/cache paths. Python tooling tests, Node appearance fixtures, version-policy checks and the compiler-free docs CMake configuration pass. Full generation follows this commit so versioned source links can resolve the relocated tracked files. Original commit ed20cdd6afc9a862f81a69beb7b7c18197a0df7b Consolidates unfinished plans and retires superseded study documents Keeps STATUS as the overview of capabilities and planned work, with links to supporting plans and remaining acceptance. Makes the planning catalogue an index of actual plan sections, next checkpoints and topics still awaiting plans. Preserves font-selection, Startup/UI, supplied RmlUi and upstream placeholder requirements in the short-term plan, and keeps the source invariant in the RmlUi guide. Removes the old Unicode plan, issue-shaped study and development roadmap after transferring their unique requirements and dependency-review procedure. Reconciles existing Startup coverage, hosted Windows baseline checks, text rasterizer scope and deferred platform prerequisites against current source. Repairs live links and retains historical audit links at their reviewed revision. Verification: checks 730 relative Markdown links and retired-target references; git diff --check passes. Native builds and tests are outside this documentation cleanup. Generated HTML verification follows at the regular documentation path. Original commit 77a0c8fedd3c17cc228350e49e76400fe3b9c588 Develops agent instructions for comprehensive documentation work Expands the shared documentation workflow to cover creation, improvement, auditing and maintenance; established ownership and navigation; beginner and expert layers; complete examples; discovery; evidence and completion checks. Adds focused API guidance for every meaningful exposed entity, including members, overloads, enum values and inherited content. Prefers trailing field briefs and detailed contracts outside class bodies, with qualified Doxygen examples and explicit current header-only extraction/cache limitations. Requires representative XML and rendered-association checks before bulk API comment work, supported manual handoffs and accounting for the full entity scope. Routes agents through the focused guide without duplicating repository policy. Verification: checks31 relative documentation links, Markdown structure and git diff whitespace; reviews source/configuration claims and official Doxygen placement/command documentation. Three agent reviews find no remaining material gap. Complex Doxygen association and rendered pilot checks remain stage2 work. This preparation changes no API comments, generated tooling or published pages. Original commit 9a19cc553acc8b08b656aba3b439097a813ccc66 Adds version-relative manual links to generated API comments Resolves cheryl-guide document targets through the Sphinx builder after Doxygen and Breathe rendering. Rejects unknown or escaping documents and preserves the link label and fragment without hardcoded hosting or generated filenames. Documents the checked source-comment form and covers valid, optional-suffix and invalid targets with focused repository-link fixtures. Verification:12 repository-link tests pass. A disposable Doxygen1.18/Breathe/ Sphinx pilot verifies trailing fields/enumerators, detached method and field contracts, overloads, class/member templates, a nested trailing-return template, a constrained method, virtual methods and a relative manual section link. No native project code was built or executed. Original commit 460cc879be89e41427bd10ea189f4746a18f3ba8 Preserves source declaration semantics in generated API output Corrects specialization bases only when the declared source class head can be matched unambiguously against XML. Removes bases incorrectly copied from the primary template without guessing alias resolution or changing C++ declarations. Carries retained constexpr flags into types consumed by Breathe so function and variable signatures preserve the qualifier. Excludes six mechanical include guards and two internal visibility helpers from public API publication while preserving public consumer macros and all headers. Verification:29 build-wrapper/metadata fixtures and8 configuration checks pass. A real Doxygen/Breathe/Sphinx pilot renders the duration trait specialization with only true_type and the variable template with constexpr. Native engine code is unchanged and was not built or executed for these tooling corrections. Original commit 1d4fa114d7c575f35dd83ca7ebee17f6919080a0 Documents exposed engine API contracts and exception references Covers the selected public headers and the installed exception owner with source-checked type, field, operation, overload, template and enum contracts. Keeps declaration bodies compact with trailing member descriptions and detached method contracts, and links significant APIs to their owning manual guides. Includes the public exception header explicitly in extraction without exposing unrelated internals, filters the GLM implementation configuration macro, and covers inventory selection with a focused regression. Records verified Doxygen association limits in the agent API instructions. Checks: all 211 headers retain their non-comment C++ tokens; 30 build-tooling and 8 configuration fixtures pass. Actual Doxygen and Sphinx render the reference without warnings. The generated-link check found one manual fragment spelling, now corrected for the final site generation. No native engine build or execution was performed. Original commit 3179d8c057416cd4f02073f31f485c549acc2c77 Improves published manual tasks and current integration contracts Reviews all 91 audited manual pages and improves 78 while preserving the agreed navigation, existing routes and useful contracts. Corrects material defaults, worker and SDK dependency linkage, runtime packaging, release gates and compiler-cache ownership against their source owners. Completes the copied-project launch checkpoint and Controller/Character/HUD handoffs; adds concrete initialization, future adoption, camera and selective asset-loading examples. Adds concise entry choices and local routes into deeper lifecycle, failure, timing, input, resource and validation contracts. Documents the current module linkage/export scaffold and API extraction registration, including standalone CTest enablement, without changing the older CMake template. Distinguishes implemented automation from outstanding hosted/native acceptance. Verification before commit: source comparisons, independent review of example insertion and contracts, unchanged navigation blocks and clean diff checks. Final complete documentation generation follows from the committed revision; native examples were not compiled or executed. Original commit a0127740d92f6aa354365d7e5ada7c7cbe69bdfd Fixes Markdown heading references after site validation Uses MyST heading slugs for Markdown references and final HTML fragments for API guide links. Points current Engine Systems lookups at Find a system while preserving the old reading-order HTML bookmark. Records the verified fragment distinction in agent authoring guidance so future edits do not substitute rendered IDs into source links that use a different resolver. The earlier full generation exposed these warning failures; the complete site is regenerated from this correction. Original commit 29e5b76d0ab7003fe60db67c310e6bfcfe7e37bf Resolves GUI setup links through their explicit label References the shared source-module-selection label with the Sphinx ref role. It labels an explanatory paragraph rather than a heading slug, so a page.md fragment cannot resolve it through the MyST heading lookup. Original commit 3ffdbdf80ecb78f52965aab91c2e97f9b1e9568e Updates .gitignore Original commit fc2cf56eb73bc85671a7e4987f23e56fbd1b2dd3 Decomposes agent instructions into indexed work guides Replaces the monolithic root AGENTS.md with essential safeguards and a canonical index into docs/for-agents. Separates shared workflow, investigation and planning, architecture, implementation, testing and Windows verification, testing-request maintenance, Git, and documentation generation. Retains the established API and writing guides and routes scoped documentation instructions through their owners. Preserves the existing progression, shared-tree, operation-permission, resource, queue, documentation-maintenance and code-convention requirements. Adds concrete methods and completion evidence for design and implementation without granting new scope or requiring persistent planning artifacts. Records the user-confirmed fixes-develop squash strategy: consolidate distinct source commits once, retain every original full commit message, preserve source refs, and verify the aggregate tree against the reviewed integration tip. Checks: verifies 146 local references across 14 instruction/entry files, checks policy parity against the original instructions, walks representative task routes, and passes git diff --check. These repository-only changes do not alter published site content or engine code; no native build or execution was required. --- .github/ci/test_versions.py | 6 +- .github/workflows/documentation.yml | 22 +- .gitignore | 17 +- .readthedocs.yaml | 2 +- AGENTS.md | 261 +- README.md | 71 +- cmake/CherylDocumentation.cmake | 10 +- docs/AGENTS.md | 21 + docs/README.md | 179 +- docs/STATUS.md | 137 +- docs/architecture/assembling-engine.md | 109 - docs/architecture/index.md | 16 - docs/development/modules.md | 250 - docs/external-work/rmlui-placeholder-issue.md | 249 - docs/for-agents/README.md | 50 + docs/for-agents/api-documentation.md | 254 + docs/for-agents/architecture.md | 127 + docs/for-agents/documentation-building.md | 67 + docs/for-agents/documentation.md | 417 + docs/for-agents/git.md | 109 + docs/for-agents/implementation.md | 139 + docs/for-agents/investigation-and-planning.md | 91 + docs/for-agents/testing-requests.md | 30 + docs/for-agents/testing.md | 74 + docs/for-agents/workflow.md | 114 + docs/generation/static/appearance.js | 49 - .../generation/tests/test_repository_links.py | 201 - docs/getting-started/create-a/enemy.md | 79 - docs/getting-started/create-a/index.md | 28 - docs/getting-started/create-a/player.md | 77 - docs/getting-started/how-to/bind-input.md | 97 - docs/getting-started/how-to/draw-sprite.md | 87 - docs/getting-started/how-to/index.md | 27 - docs/getting-started/how-to/update-text.md | 139 - docs/getting-started/how-to/use-camera.md | 78 - docs/guides/index.md | 23 - docs/index.md | 99 - docs/modules/debug-terminal-linux.md | 6 - docs/modules/index.md | 57 - docs/modules/miniaudio.md | 6 - docs/modules/native-glfw.md | 6 - docs/modules/opengl.md | 6 - docs/modules/rmlui.md | 6 - docs/modules/startup.md | 6 - docs/modules/tgui.md | 6 - docs/modules/workers.md | 6 - docs/planning/README.md | 78 +- .../develop-review-and-development-plan.md | 157 - docs/planning/long-term/README.md | 91 +- .../long-term/asset-manifest-metadata.md | 6 +- .../planning/long-term/platform-acceptance.md | 39 +- docs/planning/mid-term/README.md | 11 +- docs/planning/short-term/README.md | 168 + docs/planning/short-term/unicode-text.md | 41 - docs/{ => site}/api/index.md | 30 +- docs/site/architecture/assembling-engine.md | 41 + docs/site/architecture/index.md | 19 + .../architecture/interfaces-adapters.md | 22 +- .../architecture/ownership-cleanup.md | 2 +- .../architecture/threads-data-flow.md | 7 +- docs/{ => site}/assets/asset-loading.md | 21 +- docs/{ => site}/assets/asset-manifests.md | 55 +- .../assets/asset-values-and-playback.md | 37 +- docs/{ => site}/assets/catalog.md | 56 +- .../assets/file-and-font-discovery.md | 37 +- docs/{ => site}/assets/index.md | 11 +- docs/{ => site}/assets/text-encoding.md | 2 + docs/{ => site}/assets/text-layout.md | 230 +- docs/{ => site}/audio/index.md | 2 + .../development/architecture-validation.md | 91 +- docs/{ => site}/development/building.md | 125 +- docs/{ => site}/development/ci.md | 127 +- docs/{ => site}/development/code-style.md | 0 .../development/consuming-engine.md | 188 +- docs/{ => site}/development/contributing.md | 39 +- docs/{ => site}/development/documentation.md | 85 +- .../development/logging-acceptance.md | 14 +- docs/site/development/modules.md | 309 + .../development/native-desktop-checks.md | 27 +- .../development/publishing-documentation.md | 21 +- docs/{ => site}/development/testing.md | 51 +- docs/{ => site}/development/ui-adapters.md | 62 +- docs/{ => site}/diagnostics/index.md | 11 +- docs/{ => site}/engine-systems/assets.md | 17 +- docs/{ => site}/engine-systems/audio.md | 8 +- docs/{ => site}/engine-systems/diagnostics.md | 6 +- docs/{ => site}/engine-systems/dispatch.md | 15 +- docs/{ => site}/engine-systems/display.md | 6 +- docs/{ => site}/engine-systems/events.md | 2 +- docs/{ => site}/engine-systems/gui.md | 12 +- docs/{ => site}/engine-systems/index.md | 34 +- docs/{ => site}/engine-systems/input.md | 45 +- docs/{ => site}/engine-systems/logging.md | 13 +- docs/{ => site}/engine-systems/memory.md | 7 +- docs/{ => site}/engine-systems/rendering.md | 20 +- docs/{ => site}/engine-systems/resources.md | 9 +- docs/{ => site}/engine-systems/runtime.md | 16 +- docs/{ => site}/engine-systems/state.md | 9 +- docs/{ => site}/engine-systems/text.md | 50 +- docs/{ => site}/engine-systems/utilities.md | 7 +- docs/{ => site}/engine-systems/workers.md | 9 +- docs/{ => site}/generation/CMakeLists.txt | 2 +- docs/{ => site}/generation/Doxyfile.in | 4 +- docs/{ => site}/generation/bootstrap.py | 5 +- docs/{ => site}/generation/build.py | 90 +- docs/{ => site}/generation/check.py | 0 docs/{ => site}/generation/conf.py | 19 +- .../generation/extensions/api_rendering.py | 0 .../generation/extensions/cpp_references.py | 0 .../generation/extensions/incremental_api.py | 30 +- .../generation/extensions/inline_api.py | 0 .../generation/extensions/repository_links.py | 49 +- docs/{ => site}/generation/native-tools.json | 0 docs/{ => site}/generation/requirements.txt | 0 docs/{ => site}/generation/source_revision.py | 0 docs/site/generation/static/appearance.js | 131 + .../generation/static/copy-code.css | 0 .../{ => site}/generation/static/copy-code.js | 0 .../generation/static/hierarchy.css | 0 docs/{ => site}/generation/static/layout.css | 0 docs/{ => site}/generation/static/theme.css | 0 .../generation/static/typography.css | 0 .../generation/templates/layout.html | 0 docs/site/generation/tests/test_appearance.js | 495 + .../generation/tests/test_bootstrap.py | 0 .../{ => site}/generation/tests/test_build.py | 123 +- .../{ => site}/generation/tests/test_check.py | 0 .../generation/tests/test_configuration.py | 54 +- .../generation/tests/test_cpp_references.py | 0 .../tests/test_incremental_navigation.py | 137 + docs/site/generation/tests/test_navigation.py | 222 + .../generation/tests/test_repository_links.py | 240 + .../getting-started/create-a/character.md | 181 + .../getting-started/create-a/controller.md | 112 + .../getting-started/create-a/hud.md | 120 +- docs/site/getting-started/create-a/index.md | 33 + docs/{ => site}/getting-started/first-game.md | 189 +- .../getting-started/how-to/animate-sprite.md | 31 +- .../site/getting-started/how-to/bind-input.md | 190 + .../getting-started/how-to/create-gui.md | 54 +- .../getting-started/how-to/draw-sprite.md | 119 + docs/site/getting-started/how-to/index.md | 172 + .../getting-started/how-to/update-text.md | 123 + .../site/getting-started/how-to/use-camera.md | 87 + .../getting-started/project-setup.md | 46 +- docs/{ => site}/gui/index.md | 48 +- docs/site/guides/index.md | 27 + docs/site/index.md | 48 + docs/{ => site}/input/index.md | 7 +- docs/site/modules/debug-terminal-linux.md | 160 + docs/site/modules/index.md | 58 + docs/site/modules/miniaudio.md | 212 + docs/site/modules/native-glfw.md | 325 + docs/site/modules/opengl.md | 142 + docs/site/modules/rmlui.md | 430 + docs/site/modules/startup.md | 107 + docs/site/modules/tgui.md | 416 + docs/site/modules/workers.md | 103 + docs/{ => site}/rendering/index.md | 10 +- .../rendering/pipelines-and-materials.md | 193 +- docs/{ => site}/resources/legacy-ffont.md | 21 +- .../{ => site}/resources/resource-lifetime.md | 2 +- .../resources/resource-residency.md | 24 +- docs/{ => site}/runtime/audio.md | 10 +- .../runtime/display-and-window-contract.md | 21 +- docs/{ => site}/runtime/event-delivery.md | 6 +- docs/{ => site}/runtime/failure-reporting.md | 22 +- docs/{ => site}/runtime/index.md | 11 +- docs/{ => site}/runtime/input-state-model.md | 116 +- docs/{ => site}/runtime/logging.md | 67 +- .../runtime/runtime-frame-boundary.md | 19 +- docs/{ => site}/runtime/simulation-timing.md | 19 +- .../runtime/subsystem-diagnostics.md | 4 +- docs/{ => site}/runtime/thread-dispatch.md | 71 + docs/{ => site}/runtime/utility-contracts.md | 17 +- docs/{ => site}/runtime/worker-execution.md | 24 +- docs/site/text/index.md | 23 + docs/{ => site}/utilities/index.md | 6 +- docs/testing-requests.md | 17 +- docs/text/index.md | 17 - generate-docs.sh | 2 +- projects/apps/demo/README.md | 57 +- projects/apps/demo/src/demo-game.cpp | 93 +- projects/apps/demo/src/rmlui-demo.cpp | 19 +- projects/apps/demo/src/rmlui-demo.h | 2 +- projects/apps/demo/src/tgui-demo.cpp | 6 +- .../assets/graphics/graphics-manifests.json | 2 +- .../assets/graphics/shaders/first-game.frag | 12 - .../assets/graphics/shaders/first-game.vert | 16 - .../assets/graphics/shaders/materials.json | 77 - projects/apps/first-game/src/first-game.cpp | 34 +- projects/apps/first-game/src/first-game.h | 1 - projects/apps/tutorial-game/CMakeLists.txt | 20 +- projects/apps/tutorial-game/README.md | 44 +- .../assets/graphics/graphics-manifests.json | 2 +- .../assets/graphics/shaders/first-game.frag | 12 - .../assets/graphics/shaders/first-game.vert | 16 - .../assets/graphics/shaders/materials.json | 77 - .../tutorial-game/src/character-command.h | 7 + projects/apps/tutorial-game/src/character.h | 59 + projects/apps/tutorial-game/src/controls.cpp | 84 - projects/apps/tutorial-game/src/controls.h | 34 - projects/apps/tutorial-game/src/enemy.h | 56 - projects/apps/tutorial-game/src/game.cpp | 87 +- projects/apps/tutorial-game/src/game.h | 23 +- projects/apps/tutorial-game/src/hud.h | 117 +- projects/apps/tutorial-game/src/main.cpp | 2 +- .../tutorial-game/src/patrol-controller.h | 38 + .../tutorial-game/src/player-controller.h | 37 + projects/apps/tutorial-game/src/player.h | 52 - .../apps/tutorial-game/tests/gameplay.cpp | 92 + projects/engine/include/cheryl/assets.h | 5 + .../cheryl/assets/definitions/animation.h | 55 +- .../include/cheryl/assets/definitions/grid.h | 64 +- .../cheryl/assets/definitions/manifest.h | 30 +- .../cheryl/assets/definitions/pipeline.h | 81 +- .../cheryl/assets/definitions/shader-assets.h | 106 +- .../cheryl/assets/definitions/sprite.h | 32 +- .../cheryl/assets/definitions/tileset.h | 100 +- .../cheryl/assets/geometry/grid-geometry.h | 16 +- .../engine/include/cheryl/assets/resources.h | 5 + .../cheryl/assets/resources/decoded-image.h | 26 +- .../cheryl/assets/resources/geometry2d.h | 51 +- .../include/cheryl/assets/resources/image.h | 21 + .../cheryl/assets/resources/parameters.h | 94 +- .../cheryl/assets/resources/pipeline.h | 57 + .../assets/resources/resource-provider.h | 102 + .../include/cheryl/assets/resources/sampler.h | 84 +- .../assets/resources/shader-asset-builder.h | 74 + .../include/cheryl/assets/resources/shader.h | 82 +- .../cheryl/assets/selection/tile-selection.h | 93 +- .../include/cheryl/assets/submission/draw2d.h | 128 +- projects/engine/include/cheryl/assets/types.h | 5 + .../engine/include/cheryl/assets/types/2d.h | 5 + .../cheryl/assets/types/2d/base/asset2d.h | 16 +- .../include/cheryl/assets/types/2d/ffont.h | 28 +- .../include/cheryl/assets/types/2d/font.h | 43 +- .../include/cheryl/assets/types/2d/graphic.h | 48 +- .../include/cheryl/assets/types/2d/sprite.h | 117 +- .../include/cheryl/assets/types/2d/stbfont.h | 60 +- .../include/cheryl/assets/types/2d/tileset.h | 136 +- .../cheryl/assets/types/2d/unicode-text.h | 122 +- .../engine/include/cheryl/assets/types/3d.h | 5 + .../include/cheryl/assets/types/3d/mesh.h | 5 + .../include/cheryl/assets/types/primitives.h | 5 + .../cheryl/assets/types/primitives/frame.h | 96 +- .../cheryl/assets/types/primitives/pixel.h | 19 +- .../cheryl/assets/types/primitives/vertex.h | 165 +- projects/engine/include/cheryl/audio.h | 6 + projects/engine/include/cheryl/audio/clip.h | 42 +- projects/engine/include/cheryl/audio/system.h | 92 +- projects/engine/include/cheryl/cemath.h | 6 + projects/engine/include/cheryl/cheryl.h | 1 + projects/engine/include/cheryl/core.h | 6 + .../engine/include/cheryl/core/controls.h | 6 + .../cheryl/core/controls/action-context.h | 111 + .../cheryl/core/controls/action-snapshot.h | 119 +- .../cheryl/core/controls/button-codes.h | 40 +- .../cheryl/core/controls/device-binding.h | 37 +- .../cheryl/core/controls/input-accumulator.h | 13 + .../cheryl/core/controls/input-bindings.h | 116 +- .../cheryl/core/controls/input-capture.h | 45 + .../cheryl/core/controls/input-interface.h | 80 + .../cheryl/core/controls/input-record.h | 87 +- .../cheryl/core/controls/input-routing.h | 56 +- .../cheryl/core/controls/input-types.h | 19 +- .../cheryl/core/controls/poll-snapshot.h | 4 +- .../cheryl/core/controls/polling-backlog.h | 43 +- .../include/cheryl/core/controls/tick-input.h | 123 +- .../engine/include/cheryl/core/diagnostics.h | 27 +- projects/engine/include/cheryl/core/display.h | 5 + .../core/display/display-system-interface.h | 78 +- .../cheryl/core/display/framebuffer-size.h | 11 +- .../include/cheryl/core/display/monitor.h | 23 + .../include/cheryl/core/display/viewport.h | 14 +- .../cheryl/core/display/window-interface.h | 82 +- projects/engine/include/cheryl/core/engine.h | 5 + .../cheryl/core/engine/engine-context.h | 133 +- .../cheryl/core/engine/event-delivery.h | 27 + .../cheryl/core/engine/platform-dispatcher.h | 69 + .../core/engine/simulation-dispatcher.h | 69 + .../cheryl/core/engine/worker-configuration.h | 24 +- .../include/cheryl/core/engine/worker-pool.h | 242 +- .../cheryl/core/engine/worker-topology.h | 69 +- .../include/cheryl/core/game-framework.h | 5 + .../core/game-framework/abstract-game.h | 66 +- .../cheryl/core/game-framework/game-runtime.h | 79 +- .../game-framework/simulation-scheduler.h | 82 +- .../cheryl/core/game-framework/tick-context.h | 46 +- projects/engine/include/cheryl/core/logging.h | 5 + .../cheryl/core/logging/backend-guard.h | 14 +- .../cheryl/core/logging/compile-policy.h | 46 +- .../include/cheryl/core/logging/config.h | 41 +- .../include/cheryl/core/logging/log-names.h | 12 +- .../engine/include/cheryl/core/logging/log.h | 290 +- .../include/cheryl/core/logging/logger.h | 253 +- .../cheryl/core/logging/process-lifetime.h | 38 + .../include/cheryl/core/logging/types.h | 23 +- .../engine/include/cheryl/core/rendering.h | 5 + .../include/cheryl/core/rendering/camera.h | 88 +- .../cheryl/core/rendering/clip-region.h | 73 +- .../cheryl/core/rendering/draw-packet.h | 54 +- .../core/rendering/presentation-surface.h | 11 + .../cheryl/core/rendering/render-frame.h | 107 +- .../include/cheryl/core/rendering/renderer.h | 68 + .../engine/include/cheryl/core/resources.h | 5 + .../cheryl/core/resources/allocators.h | 5 + .../cheryl/core/resources/asset-management.h | 5 + .../resources/asset-management/asset-loader.h | 191 +- .../asset-management/file-registry.h | 45 + .../resources/asset-management/font-mgr.h | 36 +- .../asset-management/manifest-loader.h | 59 +- .../resources/asset-management/material-mgr.h | 30 + .../asset-management/shader-asset-mgr.h | 57 +- .../resources/asset-management/shader-mgr.h | 29 + .../resources/asset-management/sprite-mgr.h | 52 +- .../resources/asset-management/texture-mgr.h | 42 + .../resources/asset-management/tileset-mgr.h | 20 + .../include/cheryl/core/resources/fileio.h | 5 + .../cheryl/core/resources/fileio/file-mgr.h | 48 +- .../core/resources/fileio/fonts-system.h | 5 +- .../include/cheryl/core/resources/memory.h | 5 + .../memory/allocators/default-allocator.hpp | 143 +- .../memory/allocators/object-allocator.hpp | 140 +- .../allocators/object-pool-allocator.hpp | 176 +- .../core/resources/memory/managed-block.hpp | 9 + .../cheryl/core/resources/memory/mem-mgr.h | 41 +- .../cheryl/core/resources/memory/mem-mgr.hpp | 57 + .../core/resources/memory/release-context.h | 15 + .../cheryl/core/resources/memory/shared.h | 3 + .../cheryl/core/resources/memory/typedefs.h | 7 +- .../include/cheryl/core/resources/objects.h | 5 + .../cheryl/core/resources/objects/factory.hpp | 28 +- .../resources/objects/object-construction.hpp | 58 +- .../resources/objects/object-reservation.hpp | 46 +- .../cheryl/core/resources/objects/pool.h | 69 +- .../cheryl/core/resources/objects/pool.hpp | 38 + .../engine/include/cheryl/core/subsystems.h | 5 + .../cheryl/core/subsystems/event-bus.h | 159 +- .../cheryl/core/subsystems/event-channel.h | 18 + .../cheryl/core/subsystems/event-system.h | 72 + projects/engine/include/cheryl/engine.h | 5 + projects/engine/include/cheryl/enums.h | 6 + .../engine/include/cheryl/enums/fit-type.h | 7 +- .../engine/include/cheryl/enums/gfx-mode.h | 5 +- .../include/cheryl/enums/shader-types.h | 16 +- .../engine/include/cheryl/enums/window-mode.h | 7 +- projects/engine/include/cheryl/export.h | 109 +- projects/engine/include/cheryl/math/anchor.h | 279 +- projects/engine/include/cheryl/math/binary.h | 36 +- projects/engine/include/cheryl/math/bytes.h | 11 +- projects/engine/include/cheryl/math/fit.h | 17 +- .../engine/include/cheryl/math/pointers.h | 89 +- .../include/cheryl/math/string-numbers.h | 27 +- projects/engine/include/cheryl/math/time.h | 48 +- projects/engine/include/cheryl/modules.h | 2 + projects/engine/include/cheryl/support.h | 2 + projects/engine/include/cheryl/templates.h | 6 + .../include/cheryl/templates/asset-mgr.h | 132 +- .../cheryl/templates/block-transactions.h | 102 + .../engine/include/cheryl/templates/block.h | 369 +- .../engine/include/cheryl/templates/delta.h | 47 +- .../cheryl/templates/observed-variables.h | 60 +- .../include/cheryl/templates/singleton.h | 75 +- .../include/cheryl/templates/state-machine.h | 55 + .../include/cheryl/templates/state-tracker.h | 41 + .../cheryl/templates/versioned-variable.h | 52 +- .../include/cheryl/text/font-collection.h | 70 +- .../include/cheryl/text/font-rasterizer.h | 126 +- .../engine/include/cheryl/text/font-service.h | 126 + projects/engine/include/cheryl/text/font.h | 76 + projects/engine/include/cheryl/text/layout.h | 110 +- .../engine/include/cheryl/text/text-updater.h | 98 + projects/engine/include/cheryl/text/utf8.h | 34 +- .../engine/include/internals/exceptions.h | 181 + .../assets/resources/shader-asset-builder.cpp | 14 + .../engine/src/assets/submission/draw2d.cpp | 15 +- projects/engine/src/assets/types/2d/ffont.cpp | 2 +- .../types/2d/font-stb-allocation-internal.h | 3 +- .../assets/types/2d/font-upload-internal.h | 3 +- .../engine/src/assets/types/2d/graphic.cpp | 13 +- .../engine/src/assets/types/2d/stbfont.cpp | 25 +- .../src/assets/types/2d/unicode-text.cpp | 17 +- .../core/controls/action-context-internal.h | 29 + .../src/core/controls/action-context.cpp | 53 + .../src/core/controls/input-bindings.cpp | 152 +- .../src/core/controls/input-interface.cpp | 1 + .../engine/src/core/controls/tick-input.cpp | 37 +- .../resources/asset-management/sprite-mgr.cpp | 6 +- .../asset-management/tileset-mgr.cpp | 6 +- projects/engine/src/text/font-collection.cpp | 53 +- projects/engine/src/text/font-rasterizer.cpp | 11 + projects/engine/src/text/font-service.cpp | 311 + projects/engine/src/text/layout.cpp | 4 + projects/engine/src/text/text-updater.cpp | 175 + .../all-tests/src/core/runtime-adapter.cpp | 48 +- .../tests/all-tests/src/resources/fonts.cpp | 15 + .../tests/consumer/headers/CMakeLists.txt | 5 + .../tests/consumer/headers/aggregate.cpp | 5 + projects/engine/tests/consumer/src/fonts.cpp | 14 + projects/engine/tests/consumer/src/main.cpp | 5 +- .../tests/include/testing/font-fixture.h | 109 + .../tests/include/testing/material-builder.h | 32 + .../engine/tests/unit/src/action-contexts.cpp | 293 + .../engine/tests/unit/src/font-service.cpp | 277 + .../tests/unit/src/runtime-contract.cpp | 1 + .../engine/tests/unit/src/text-updater.cpp | 501 + .../engine/tests/unit/src/unicode-text.cpp | 50 +- projects/modules/README.md | 29 +- projects/modules/audio/miniaudio/README.md | 207 +- .../include/cheryl/backends/miniaudio.h | 63 +- .../include/cheryl/modules/miniaudio.h | 3 +- projects/modules/graphics/opengl/README.md | 126 +- .../opengl/include/cheryl/backends/opengl.h | 5 + .../include/cheryl/backends/opengl/context.h | 26 +- .../include/cheryl/backends/opengl/gl.h | 20 +- .../cheryl/backends/opengl/glfw-backend.h | 32 +- .../cheryl/backends/opengl/glfw-context.h | 35 + .../cheryl/backends/opengl/glslprogram.h | 228 +- .../include/cheryl/backends/opengl/pipeline.h | 74 +- .../include/cheryl/backends/opengl/renderer.h | 91 + .../backends/opengl/resource-lifetime.h | 146 +- .../backends/opengl/resource-provider.h | 83 +- .../include/cheryl/backends/opengl/sampler.h | 30 + .../include/cheryl/backends/opengl/texture.h | 71 +- .../backends/opengl/vertex-array-object.h | 102 +- .../opengl/include/cheryl/modules/opengl.h | 3 +- .../src/backends/opengl/builtin-materials.cpp | 80 +- .../tests/acceptance/src/native-opengl.cpp | 85 + .../platform/debug-terminal-linux/README.md | 154 +- .../cheryl/modules/debug-terminal-linux.h | 1 + .../cheryl/core/debug-terminal/startup.h | 14 + .../include/cheryl/support/debug-terminal.h | 1 + .../modules/platform/native-glfw/README.md | 321 +- .../include/cheryl/backends/native-glfw.h | 6 + .../cheryl/core/controls/input-system.h | 81 +- .../cheryl/core/display/display-system.h | 79 +- .../include/cheryl/core/display/window.h | 73 +- .../include/cheryl/modules/native-glfw.h | 4 +- projects/modules/runtime/startup/README.md | 107 +- .../include/cheryl/application/startup.h | 177 +- .../include/cheryl/backends/opengl/startup.h | 4 + .../include/cheryl/core/engine/startup.h | 5 + .../startup/include/cheryl/modules/startup.h | 1 + .../include/cheryl/support/opengl-startup.h | 5 + .../startup/include/cheryl/support/startup.h | 5 + projects/modules/runtime/workers/README.md | 103 +- .../engine/worker-topology/configuration.h | 33 + .../core/engine/worker-topology/hwloc.h | 8 + .../workers/include/cheryl/modules/workers.h | 5 + .../cheryl/support/worker-topology-hwloc.h | 1 + .../cheryl/support/worker-topology-json.h | 1 + projects/modules/ui/rmlui/README.md | 356 +- .../ui/rmlui/include/cheryl/modules/rmlui.h | 4 +- .../ui/rmlui/include/cheryl/ui/rmlui/input.h | 9 + .../rmlui/include/cheryl/ui/rmlui/rendering.h | 126 +- .../ui/rmlui/include/cheryl/ui/rmlui/scene.h | 46 +- .../rmlui/include/cheryl/ui/rmlui/session.h | 120 +- projects/modules/ui/rmlui/src/session.cpp | 144 +- .../ui/rmlui/tests/consumer/src/main.cpp | 14 + .../ui/rmlui/tests/unit/src/session.cpp | 172 +- projects/modules/ui/tgui/README.md | 378 +- .../ui/tgui/include/cheryl/modules/tgui.h | 4 +- .../ui/tgui/include/cheryl/ui/tgui/input.h | 1 + .../ui/tgui/include/cheryl/ui/tgui/layout.h | 28 +- .../tgui/include/cheryl/ui/tgui/rendering.h | 98 +- .../ui/tgui/include/cheryl/ui/tgui/scene.h | 46 +- .../ui/tgui/include/cheryl/ui/tgui/session.h | 110 +- projects/modules/ui/tgui/src/font.cpp | 82 +- projects/modules/ui/tgui/src/font.h | 13 +- projects/modules/ui/tgui/src/session.cpp | 76 + .../ui/tgui/tests/consumer/src/main.cpp | 12 + .../ui/tgui/tests/unit/src/session.cpp | 126 +- .../documentation-usefulness/api-scores.csv | 991 + .../documentation-usefulness/audit-data.json | 44497 ++++++++++++++++ .../authored-scores.csv | 116 + .../published-scores.csv | 92 + reports/documentation-usefulness/report.md | 638 + 478 files changed, 69419 insertions(+), 7114 deletions(-) create mode 100644 docs/AGENTS.md delete mode 100644 docs/architecture/assembling-engine.md delete mode 100644 docs/architecture/index.md delete mode 100644 docs/development/modules.md delete mode 100644 docs/external-work/rmlui-placeholder-issue.md create mode 100644 docs/for-agents/README.md create mode 100644 docs/for-agents/api-documentation.md create mode 100644 docs/for-agents/architecture.md create mode 100644 docs/for-agents/documentation-building.md create mode 100644 docs/for-agents/documentation.md create mode 100644 docs/for-agents/git.md create mode 100644 docs/for-agents/implementation.md create mode 100644 docs/for-agents/investigation-and-planning.md create mode 100644 docs/for-agents/testing-requests.md create mode 100644 docs/for-agents/testing.md create mode 100644 docs/for-agents/workflow.md delete mode 100644 docs/generation/static/appearance.js delete mode 100644 docs/generation/tests/test_repository_links.py delete mode 100644 docs/getting-started/create-a/enemy.md delete mode 100644 docs/getting-started/create-a/index.md delete mode 100644 docs/getting-started/create-a/player.md delete mode 100644 docs/getting-started/how-to/bind-input.md delete mode 100644 docs/getting-started/how-to/draw-sprite.md delete mode 100644 docs/getting-started/how-to/index.md delete mode 100644 docs/getting-started/how-to/update-text.md delete mode 100644 docs/getting-started/how-to/use-camera.md delete mode 100644 docs/guides/index.md delete mode 100644 docs/index.md delete mode 100644 docs/modules/debug-terminal-linux.md delete mode 100644 docs/modules/index.md delete mode 100644 docs/modules/miniaudio.md delete mode 100644 docs/modules/native-glfw.md delete mode 100644 docs/modules/opengl.md delete mode 100644 docs/modules/rmlui.md delete mode 100644 docs/modules/startup.md delete mode 100644 docs/modules/tgui.md delete mode 100644 docs/modules/workers.md delete mode 100644 docs/planning/develop-review-and-development-plan.md create mode 100644 docs/planning/short-term/README.md delete mode 100644 docs/planning/short-term/unicode-text.md rename docs/{ => site}/api/index.md (77%) create mode 100644 docs/site/architecture/assembling-engine.md create mode 100644 docs/site/architecture/index.md rename docs/{ => site}/architecture/interfaces-adapters.md (91%) rename docs/{ => site}/architecture/ownership-cleanup.md (97%) rename docs/{ => site}/architecture/threads-data-flow.md (93%) rename docs/{ => site}/assets/asset-loading.md (88%) rename docs/{ => site}/assets/asset-manifests.md (82%) rename docs/{ => site}/assets/asset-values-and-playback.md (91%) rename docs/{ => site}/assets/catalog.md (66%) rename docs/{ => site}/assets/file-and-font-discovery.md (79%) rename docs/{ => site}/assets/index.md (56%) rename docs/{ => site}/assets/text-encoding.md (97%) rename docs/{ => site}/assets/text-layout.md (56%) rename docs/{ => site}/audio/index.md (89%) rename docs/{ => site}/development/architecture-validation.md (85%) rename docs/{ => site}/development/building.md (81%) rename docs/{ => site}/development/ci.md (58%) rename docs/{ => site}/development/code-style.md (100%) rename docs/{ => site}/development/consuming-engine.md (76%) rename docs/{ => site}/development/contributing.md (55%) rename docs/{ => site}/development/documentation.md (72%) rename docs/{ => site}/development/logging-acceptance.md (80%) create mode 100644 docs/site/development/modules.md rename docs/{ => site}/development/native-desktop-checks.md (86%) rename docs/{ => site}/development/publishing-documentation.md (88%) rename docs/{ => site}/development/testing.md (70%) rename docs/{ => site}/development/ui-adapters.md (80%) rename docs/{ => site}/diagnostics/index.md (60%) rename docs/{ => site}/engine-systems/assets.md (85%) rename docs/{ => site}/engine-systems/audio.md (92%) rename docs/{ => site}/engine-systems/diagnostics.md (95%) rename docs/{ => site}/engine-systems/dispatch.md (86%) rename docs/{ => site}/engine-systems/display.md (93%) rename docs/{ => site}/engine-systems/events.md (98%) rename docs/{ => site}/engine-systems/gui.md (86%) rename docs/{ => site}/engine-systems/index.md (91%) rename docs/{ => site}/engine-systems/input.md (74%) rename docs/{ => site}/engine-systems/logging.md (89%) rename docs/{ => site}/engine-systems/memory.md (93%) rename docs/{ => site}/engine-systems/rendering.md (82%) rename docs/{ => site}/engine-systems/resources.md (93%) rename docs/{ => site}/engine-systems/runtime.md (89%) rename docs/{ => site}/engine-systems/state.md (92%) rename docs/{ => site}/engine-systems/text.md (60%) rename docs/{ => site}/engine-systems/utilities.md (94%) rename docs/{ => site}/engine-systems/workers.md (92%) rename docs/{ => site}/generation/CMakeLists.txt (90%) rename docs/{ => site}/generation/Doxyfile.in (93%) rename docs/{ => site}/generation/bootstrap.py (99%) rename docs/{ => site}/generation/build.py (85%) rename docs/{ => site}/generation/check.py (100%) rename docs/{ => site}/generation/conf.py (91%) rename docs/{ => site}/generation/extensions/api_rendering.py (100%) rename docs/{ => site}/generation/extensions/cpp_references.py (100%) rename docs/{ => site}/generation/extensions/incremental_api.py (73%) rename docs/{ => site}/generation/extensions/inline_api.py (100%) rename docs/{ => site}/generation/extensions/repository_links.py (89%) rename docs/{ => site}/generation/native-tools.json (100%) rename docs/{ => site}/generation/requirements.txt (100%) rename docs/{ => site}/generation/source_revision.py (100%) create mode 100644 docs/site/generation/static/appearance.js rename docs/{ => site}/generation/static/copy-code.css (100%) rename docs/{ => site}/generation/static/copy-code.js (100%) rename docs/{ => site}/generation/static/hierarchy.css (100%) rename docs/{ => site}/generation/static/layout.css (100%) rename docs/{ => site}/generation/static/theme.css (100%) rename docs/{ => site}/generation/static/typography.css (100%) rename docs/{ => site}/generation/templates/layout.html (100%) create mode 100644 docs/site/generation/tests/test_appearance.js rename docs/{ => site}/generation/tests/test_bootstrap.py (100%) rename docs/{ => site}/generation/tests/test_build.py (76%) rename docs/{ => site}/generation/tests/test_check.py (100%) rename docs/{ => site}/generation/tests/test_configuration.py (58%) rename docs/{ => site}/generation/tests/test_cpp_references.py (100%) create mode 100644 docs/site/generation/tests/test_incremental_navigation.py create mode 100644 docs/site/generation/tests/test_navigation.py create mode 100644 docs/site/generation/tests/test_repository_links.py create mode 100644 docs/site/getting-started/create-a/character.md create mode 100644 docs/site/getting-started/create-a/controller.md rename docs/{ => site}/getting-started/create-a/hud.md (57%) create mode 100644 docs/site/getting-started/create-a/index.md rename docs/{ => site}/getting-started/first-game.md (81%) rename docs/{ => site}/getting-started/how-to/animate-sprite.md (66%) create mode 100644 docs/site/getting-started/how-to/bind-input.md rename docs/{ => site}/getting-started/how-to/create-gui.md (84%) create mode 100644 docs/site/getting-started/how-to/draw-sprite.md create mode 100644 docs/site/getting-started/how-to/index.md create mode 100644 docs/site/getting-started/how-to/update-text.md create mode 100644 docs/site/getting-started/how-to/use-camera.md rename docs/{ => site}/getting-started/project-setup.md (89%) rename docs/{ => site}/gui/index.md (73%) create mode 100644 docs/site/guides/index.md create mode 100644 docs/site/index.md rename docs/{ => site}/input/index.md (58%) create mode 100644 docs/site/modules/debug-terminal-linux.md create mode 100644 docs/site/modules/index.md create mode 100644 docs/site/modules/miniaudio.md create mode 100644 docs/site/modules/native-glfw.md create mode 100644 docs/site/modules/opengl.md create mode 100644 docs/site/modules/rmlui.md create mode 100644 docs/site/modules/startup.md create mode 100644 docs/site/modules/tgui.md create mode 100644 docs/site/modules/workers.md rename docs/{ => site}/rendering/index.md (55%) rename docs/{ => site}/rendering/pipelines-and-materials.md (77%) rename docs/{ => site}/resources/legacy-ffont.md (81%) rename docs/{ => site}/resources/resource-lifetime.md (97%) rename docs/{ => site}/resources/resource-residency.md (90%) rename docs/{ => site}/runtime/audio.md (88%) rename docs/{ => site}/runtime/display-and-window-contract.md (86%) rename docs/{ => site}/runtime/event-delivery.md (97%) rename docs/{ => site}/runtime/failure-reporting.md (81%) rename docs/{ => site}/runtime/index.md (53%) rename docs/{ => site}/runtime/input-state-model.md (77%) rename docs/{ => site}/runtime/logging.md (92%) rename docs/{ => site}/runtime/runtime-frame-boundary.md (84%) rename docs/{ => site}/runtime/simulation-timing.md (82%) rename docs/{ => site}/runtime/subsystem-diagnostics.md (98%) rename docs/{ => site}/runtime/thread-dispatch.md (59%) rename docs/{ => site}/runtime/utility-contracts.md (84%) rename docs/{ => site}/runtime/worker-execution.md (91%) create mode 100644 docs/site/text/index.md rename docs/{ => site}/utilities/index.md (67%) delete mode 100644 docs/text/index.md delete mode 100644 projects/apps/first-game/assets/graphics/shaders/first-game.frag delete mode 100644 projects/apps/first-game/assets/graphics/shaders/first-game.vert delete mode 100644 projects/apps/first-game/assets/graphics/shaders/materials.json delete mode 100644 projects/apps/tutorial-game/assets/graphics/shaders/first-game.frag delete mode 100644 projects/apps/tutorial-game/assets/graphics/shaders/first-game.vert delete mode 100644 projects/apps/tutorial-game/assets/graphics/shaders/materials.json create mode 100644 projects/apps/tutorial-game/src/character-command.h create mode 100644 projects/apps/tutorial-game/src/character.h delete mode 100644 projects/apps/tutorial-game/src/controls.cpp delete mode 100644 projects/apps/tutorial-game/src/controls.h delete mode 100644 projects/apps/tutorial-game/src/enemy.h create mode 100644 projects/apps/tutorial-game/src/patrol-controller.h create mode 100644 projects/apps/tutorial-game/src/player-controller.h delete mode 100644 projects/apps/tutorial-game/src/player.h create mode 100644 projects/apps/tutorial-game/tests/gameplay.cpp create mode 100644 projects/engine/include/cheryl/core/controls/action-context.h create mode 100644 projects/engine/include/cheryl/text/font-service.h create mode 100644 projects/engine/include/cheryl/text/font.h create mode 100644 projects/engine/include/cheryl/text/text-updater.h create mode 100644 projects/engine/src/core/controls/action-context-internal.h create mode 100644 projects/engine/src/core/controls/action-context.cpp create mode 100644 projects/engine/src/text/font-service.cpp create mode 100644 projects/engine/src/text/text-updater.cpp create mode 100644 projects/engine/tests/consumer/src/fonts.cpp create mode 100644 projects/engine/tests/include/testing/font-fixture.h create mode 100644 projects/engine/tests/unit/src/action-contexts.cpp create mode 100644 projects/engine/tests/unit/src/font-service.cpp create mode 100644 projects/engine/tests/unit/src/text-updater.cpp mode change 100644 => 120000 projects/modules/audio/miniaudio/README.md mode change 100644 => 120000 projects/modules/graphics/opengl/README.md mode change 100644 => 120000 projects/modules/platform/debug-terminal-linux/README.md mode change 100644 => 120000 projects/modules/platform/native-glfw/README.md mode change 100644 => 120000 projects/modules/runtime/startup/README.md mode change 100644 => 120000 projects/modules/runtime/workers/README.md mode change 100644 => 120000 projects/modules/ui/rmlui/README.md mode change 100644 => 120000 projects/modules/ui/tgui/README.md create mode 100644 reports/documentation-usefulness/api-scores.csv create mode 100644 reports/documentation-usefulness/audit-data.json create mode 100644 reports/documentation-usefulness/authored-scores.csv create mode 100644 reports/documentation-usefulness/published-scores.csv create mode 100644 reports/documentation-usefulness/report.md diff --git a/.github/ci/test_versions.py b/.github/ci/test_versions.py index 74845c01..d9670be5 100644 --- a/.github/ci/test_versions.py +++ b/.github/ci/test_versions.py @@ -190,8 +190,8 @@ def test_repository_versions(self): def test_owners(self): expected = {versions.ENGINE, "VERSION_PROJECT_MODULE_NATIVE_GLFW", "VERSION_PROJECT_MODULE_UI_RMLUI"} self.assertEqual(versions.affected_components({"cmake/CherylOptions.cmake", "extern/glfw", "extern/rmlui"}), expected) - self.assertEqual(versions.affected_components({"projects/engine/tests/unit/src/test.cpp", "docs/runtime/logging.md", - "projects/modules/ui/rmlui/README.md", ".github/workflows/ci.yml"}), set()) + self.assertEqual(versions.affected_components({"projects/engine/tests/unit/src/test.cpp", "docs/site/runtime/logging.md", + "docs/site/modules/rmlui.md", ".github/workflows/ci.yml"}), set()) self.assertEqual(versions.affected_components({"projects/apps/first-game/src/first-game.cpp"}), set()) def test_release_target(self): @@ -288,7 +288,7 @@ def test_old_generated_merge(self): self.assertEqual(self.automation.planned_versions(state(B, newer), C), newer) def test_docs(self): - self.git.paths[B] = {"docs/development/building.md"} + self.git.paths[B] = {"docs/site/development/building.md"} planned = self.automation.planned_versions(state(), B) self.assertEqual(planned[versions.PROJECT], "4.0.1") self.assertEqual(planned[versions.ENGINE], "4.0.0") diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 566467d8..1f7dfdfa 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -40,10 +40,10 @@ jobs: - name: Check documentation tooling run: | python .github/scripts/test_sync_docs_stable.py - python -m unittest discover -s docs/generation/tests -p 'test_*.py' + python -m unittest discover -s docs/site/generation/tests -p 'test_*.py' - name: Identify documentation tool cache id: tool-cache - run: python -c 'import sys; sys.path.insert(0, "docs/generation"); from bootstrap import environment_identity; print("identity=" + environment_identity())' >> "$GITHUB_OUTPUT" + run: python -c 'import sys; sys.path.insert(0, "docs/site/generation"); from bootstrap import environment_identity; print("identity=" + environment_identity())' >> "$GITHUB_OUTPUT" - name: Describe shared Actions environment id: environment run: python .github/ci/dependencies.py environment-prefix --profile docs @@ -52,9 +52,9 @@ jobs: uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.CHERYL_DOCS_CACHE_DIR }} - key: ${{ steps.environment.outputs.environment-prefix }}-tools-${{ steps.tool-cache.outputs.identity }}-${{ hashFiles('docs/generation/requirements.txt', 'docs/generation/native-tools.json', 'docs/generation/bootstrap.py') }} + key: ${{ steps.environment.outputs.environment-prefix }}-tools-${{ steps.tool-cache.outputs.identity }}-${{ hashFiles('docs/site/generation/requirements.txt', 'docs/site/generation/native-tools.json', 'docs/site/generation/bootstrap.py') }} - name: Prepare pinned documentation tools - run: python docs/generation/bootstrap.py setup --force-pinned + run: python docs/site/generation/bootstrap.py setup --force-pinned - name: Save shared documentation tools if: steps.environment.outputs.environment-cache-writer == 'true' && steps.tools.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -63,13 +63,13 @@ jobs: key: ${{ steps.tools.outputs.cache-primary-key }} - name: Generate and check documentation run: | - mkdir -p docs/generation/generated - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean 2>&1 | tee docs/generation/generated/generation.log + mkdir -p docs/site/generation/generated + python docs/site/generation/bootstrap.py exec --force-pinned -- python docs/site/generation/build.py --clean 2>&1 | tee docs/site/generation/generated/generation.log - name: Retain checked HTML uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: documentation-html-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} - path: docs/generation/generated/html/ + path: docs/site/generation/generated/html/ if-no-files-found: error retention-days: 30 - name: Retain documentation diagnostics @@ -78,10 +78,10 @@ jobs: with: name: documentation-diagnostics-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} path: | - docs/generation/generated/*.log - docs/generation/generated/Doxyfile - docs/generation/generated/xml/ - docs/generation/generated/html/revision.json + docs/site/generation/generated/*.log + docs/site/generation/generated/Doxyfile + docs/site/generation/generated/xml/ + docs/site/generation/generated/html/revision.json if-no-files-found: warn retention-days: 30 diff --git a/.gitignore b/.gitignore index a04c02d2..8f1fe1da 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,8 @@ cmake-build*/ __pycache__/ # Generated documentation, including legacy API output -/docs/generation/generated/ -/docs/generated/ -/docs/api/generated/ +/docs/site/generation/generated/ + .git/ .idea/ @@ -23,17 +22,5 @@ notes/ *.png *.wav -assets/** -!assets/**/ -!assets/**/*.rml -!assets/**/*.json -!assets/**/*.frag -!assets/**/*.vert -!assets/fonts/* -assets/sfx -assets/graphics/**/*.txt - -!assets/graphics/ui/quadrants.png -!assets/graphics/ui/quadrants-alt.png !/projects/modules/graphics/opengl/tests/fixtures/rgba-two-rows.png !/projects/apps/first-game/assets/graphics/textures/actor.png diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 63588806..46f4fe60 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,4 +10,4 @@ build: jobs: build: html: - - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean --output "$READTHEDOCS_OUTPUT" + - python docs/site/generation/bootstrap.py exec --force-pinned -- python docs/site/generation/build.py --clean --output "$READTHEDOCS_OUTPUT" diff --git a/AGENTS.md b/AGENTS.md index 978155f8..d7f9320f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,217 +1,44 @@ -# Cheryl Engine Agent Instructions - -## General instructions - -### Work modes and progression - -Each task has one primary work mode: - -- Review and planning -- Investigation and research -- Architecture and design -- Implementation, refactoring, and tooling -- Test design -- Test implementation -- Test execution -- Verification -- Documentation audit -- Documentation maintenance - -Keep independent work objectives separated by mode. A request for review or investigation does not authorize implementation merely because a fix becomes apparent. - -A request to implement or fix behavior includes necessary investigation, updates to affected tests, focused regression coverage where useful, and verification permitted below. These supporting activities belong to the same development unit. Independent test design, broader verification campaigns, and documentation work remain separate objectives unless authorized. - -User-directed progression is the default: complete the requested objective and stop before starting an independent objective. When a bounded sequence would benefit from plan-directed progression, propose its phases and decision checkpoints before adopting it. An approved plan may authorize progression across modes without further permission for each coherent subtask. - -Stop when: - -- The requested scope is complete or an approved plan reaches a mandatory decision checkpoint. -- A material requirement, architectural decision, or invalidated assumption requires user direction. -- A necessary operation or scope expansion is not already authorized. -- Continuing would depend on unfinished user work or risk modifying protected changes. -- A blocking condition prevents safe or correct completion. - -A completed subtask or necessary supporting activity is not itself a stopping point. Continue authorized work that can safely proceed, including independent work unaffected by a blocked portion. Report any boundary, its implications, and the recommended next action. - -At handoff, distinguish implemented changes, observed verification, and outstanding or deferred work. Completing an implementation phase does not establish executable acceptance or complete a broader task with required work remaining. Optional improvements and advisory documentation checkpoints neither block completion nor authorize further work. - -### Source authority and context - -Prefer source code, configuration, and actual implementation when determining the repository's current state and behavior. - -Use documentation to understand intended architecture, requirements, design rationale, historical decisions, and planned work. Documentation may describe previous, intended, or partially implemented behavior. - -Verify documentation's claims about current implementation against the relevant code. When implementation and documented intent disagree, distinguish the observed state from the intended state rather than automatically treating either as correct. - -Acquire context purposefully. Inspect the code, documentation, history, and dependencies relevant to the current objective. Expand investigation when necessary rather than loading unrelated project context by default. - -Prefer concise phase handoffs containing established decisions, relevant constraints, outstanding work, and affected contracts. Do not create persistent handoff or planning documents unless explicitly requested. - -## Repository safety - -- Do not push commits or branches to any remote unless explicitly requested. -- Do not build or compile the project or its dependencies unless explicitly authorized by the user's request or an approved plan. -- Preserve active user runs and their outputs. Do not change their configuration, clean their build directories, or invalidate their caches as a routine preparation step. -- Do not perform opportunistic refactoring, cleanup, formatting, or API changes merely because they are convenient while working nearby. -- Report related work outside the current development unit as discovered work. Resolve necessary dependencies within the authorized scope; use the progression boundaries above when further work requires user direction. - -## Verification and resource use - -- Run the smallest relevant permitted checks before handing off an implementation. Update affected tests with the behavior they cover, and add focused regression coverage when existing checks do not adequately exercise a material change. -- Static analysis and focused script or tooling tests using mocks or disposable fixtures are permitted without separate authorization when they do not compile project or dependency code, run native project executables, install or download tools or dependencies, generate full documentation, or access devices or external services. CMake script and configuration probes that enable no compiled languages may qualify. -- Inspect an unfamiliar check before executing it, including the subprocesses it launches. A Python filename or a description such as "tooling check" does not establish that a check is lightweight. Configuration that invokes compiler or linker probes requires build authorization. -- Project or dependency builds, native test execution, full documentation generation, dependency or tool downloads or installation, and tests using devices or external services require explicit authorization. Existing authorization persists for relevant fixes and reruns within its scope. When additional authorization is necessary to establish the requested outcome, identify the missing verification and request it together rather than waiting for an unrelated blocker. -- Batch related checks and reuse compatible builds and caches. After fixing a failed check, rerun the affected checks when permitted. Broaden or repeat successful checks only when changed inputs, failures, or unresolved concerns justify it. -- Share machine resources during authorized heavy work by bounding agent-run parallelism or reducing process priority as needed. Commands supplied for the user run at normal priority with available parallelism; do not add low-priority wrappers or single-job limits to user-run commands. -- Test behavior and contracts rather than mirroring implementation details. Use mocks to check orchestration, and distinguish that coverage from executing the generated commands or consuming the resulting artifacts. -- Report what actually ran and what remains unverified. Static checks, mocked tests, and successful configuration do not establish native build, link, relocation, or runtime success. Distinguish confirmed failures, inferred causes, and fixes awaiting executable acceptance. - -### Local Windows tooling - -- On this workstation, reusable Windows tooling lives in `/mnt/projects/cheryl-windows-ci/`. Check it before downloading tools, creating another Wine environment, or reporting that Windows tooling is unavailable. If it is absent on another host, look for compatible installed tools there. -- The `tools/` directory contains Windows Python, CMake, Ninja, Git, ccache, and vcpkg. Read `env.sh` and `run.sh` for current paths, environment settings, and supported phases before using them; inspect the selected phase before execution. MSVC and the Windows SDK are installed under `/opt/msvc/`. -- Preserve `tools/`, `downloads/`, and `wine-prefix/` across sessions. Store additional reusable tools and isolated Wine prefixes under this disk-backed tooling root, preserving existing versions and active work. -- `/tmp` is memory-backed on this workstation. During RAM cleanup, preserve reusable tools and cached installers; move them to disk-backed storage when needed. Stop idle agent-owned processes and remove disposable test outputs within the authorized scope. -- These locations do not authorize builds, native tests, downloads, or changes to an active environment. The existing verification and shared-work restrictions still apply. - -## Shared working tree - -The repository is a shared human/agent working tree. Pre-existing uncommitted changes may represent deliberate work in progress and may be incomplete, temporarily inconsistent, non-functional, transitional, preparatory for later refactoring, or difficult to reason about safely. - -Before modifying affected code: - -- Inspect the current Git status and relevant pre-existing diffs. -- Treat pre-existing user changes as authoritative work in progress. Do not revert, overwrite, normalize, reformat, or replace them merely to simplify the task. -- Do not assume pre-existing changes form a valid, complete, or stable implementation baseline. -- Do not build new task work on top of pre-existing unfinished user work merely because its current behavior is understandable. -- Avoid introducing new dependencies on unfinished user work, propagating its current structure into additional code, constraining its future evolution, or otherwise increasing the amount of work that would need to change when the user's work is completed. - -A file containing user changes is not automatically off-limits. Work with it only when the requested change is independent of the unfinished work, or when the change can be made without increasing the dependency surface, constraining future evolution, or increasing the likely scope of subsequent refactoring. - -If completing the requested work would require treating unfinished user work as a stable foundation, extending its current design into other code, or making additional code depend on it, treat that as a development boundary. - -When such a boundary is encountered: - -- Preserve the existing user work unchanged where possible. -- Stop at that boundary. -- Continue with independent planned work only when it is clearly unaffected by, and does not depend on, the blocked work. -- Clearly identify what cannot be completed safely and why. - -When committing: - -- Commit only task-related changes that can be cleanly separated from pre-existing user work. -- Do not include unrelated or unfinished user work in an agent-created commit. -- If task-related work and pre-existing user work cannot be separated safely, leave the overlapping work uncommitted rather than rewriting the user's work to manufacture a clean commit. - -## Planning and execution - -Before beginning non-trivial implementation work, establish a concise plan containing coherent tasks, risky assumptions, dependency boundaries, and any required decision checkpoints. Keep it in the working context unless a persistent planning artifact is explicitly requested. - -Resolve foundational decisions and uncertain boundaries before accumulating dependent work. Follow the plan within its authorized scope, revising it when implementation reveals material new information. A plan does not authorize operations or independent work outside its approved scope. - -Complete and commit coherent development units independently after appropriate permitted checks. State any outstanding executable acceptance rather than implying that a committed implementation has been fully verified. - -### Build, tooling, SDK, and CI changes - -- Identify the affected contract, its owner, and the path from producer to consumer before changing behavior. Include relevant artifact creation, installation, packaging, relocation, runtime loading, cache reuse, and workflow activation boundaries; inspect only the portions affected by the task. -- Locate the existing authority for each policy before adding another implementation. Prefer extending that owner over duplicating policy in another wrapper, classifier, cache rule, fallback, or compatibility branch. When CMake, Python, or workflow code must express the same policy, keep their semantics aligned and cover their agreement with focused checks. -- Review the execution contexts affected by the change. Examples include project configuration versus generated CMake install or script execution, source versus installed SDK consumption, static versus shared linkage, Linux versus Windows, cold versus warm caches, and default-branch workflow prerequisites. Select relevant coverage; do not run the entire matrix for every edit. -- Preserve the intended contract when correcting a check. Trace dependency ownership before changing bundling rules or system-library exceptions. Distinguish correcting a verifier from changing what the SDK promises to supply. -- For repeated failures, revisit the shared assumption or boundary before adding another local exception. Remove superseded paths when that is part of the requested change; report larger simplification work separately rather than turning a fix into an unrequested redesign. - -## Testing requests - -Testing-request documentation is maintained only when explicitly authorized. Do not update `docs/testing-requests.md` automatically during implementation, refactoring, verification, or other non-documentation phases. - -Preserve discovered testing requirements in the task handoff until a testing-request maintenance phase is authorized. At that point, reconcile accumulated requirements against existing requests. - -These rules govern testing-request documentation. They do not defer directly related test maintenance or permitted verification from an implementation task. - -During authorized testing-request maintenance: - -- Maintain `docs/testing-requests.md` as the user-facing queue of testing needed for implemented work. Reconcile accumulated testing needs with the current source, targets, configuration, reported results, and required coverage. -- Keep a summary table near the top with exactly three columns: **TR#**, **Short description / title**, and **Long description (plain language)**. Give every active request one row, with its `TR` linked directly to its detailed section/heading. Include the same ID in that heading. Preserve request IDs when updating requests; do not renumber remaining requests or reuse retired IDs. -- Prefix each summary-table **Short description / title** cell with **Automated:** or **QA:**, matching the request type in its detailed section. -- Write each table's long description for a reader unfamiliar with engine or testing internals: explain what will be checked, why it matters, and any setup requirement or blocker affecting whether they can run it. Explain necessary jargon; keep exact commands and detailed acceptance criteria in the linked section. -- Aggregate deferred testing across development cycles. Extend an existing request when it covers the same behavior and configuration; avoid duplicate case runs through owner and aggregate runners, and batch needed build targets into as few invocations as practical. -- Label each request as **Automated** or **QA**, with its platform, prerequisites, readiness and acceptance scope. Keep development progress in the owning plan rather than duplicating its macro-task checklist here. -- For each automated request, provide a runnable fenced command block. Include required CMake configuration, explicit build targets, environment opt-ins, working directory and test selection. State when a matching existing build can be reused. -- Command blocks must establish their working directory explicitly, including QA launch blocks. Commands launched from a document may inherit that document's directory; locate the checkout root rather than assuming the shell already runs there, and preserve the caller's starting directory. -- User-run command-line CMake configuration must explicitly select Ninja with `-G Ninja`. Build and test commands use that configured build directory. -- For each QA request, provide a brief detailed summary and concise requirement bullets covering setup, actions, expected observations and required platform/runtime modes. -- Mark requests blocked when fixtures, hardware or an observation harness are unavailable, and identify the next action. Do not invent commands or count skipped, unselected or device-free checks as the missing acceptance. -- When the user reports results, reconcile requests and owning plans during the next authorized maintenance phase against the actual tested revision, configuration, platforms and coverage. Remove accepted requests, preserve unresolved portions and repair affected links; retain durable coverage limits in the relevant subject guide rather than appending routine execution logs. - -Submitting a testing request does not authorize operations that require explicit permission under Verification and resource use. - -## Documentation discipline - -Reading documentation is permitted in any work mode when relevant to understanding requirements, intent, constraints, or history. Prefer the relevant source code when establishing current behavior, and consult the relevant roadmap or subject document for direction rather than loading the entire planning folder. - -Documentation auditing and maintenance are separate work modes. Do not create, update, reorganize, or delete documentation during other work modes unless explicitly requested. - -Essential code comments and runtime logging directly required by the implementation are permitted. Do not perform general comment cleanup, logging improvements, or explanatory documentation work opportunistically. - -Defer documentation synchronization until a dedicated maintenance phase. Prefer stabilizing related implementation work before reconciling documentation. - -Recommend a documentation checkpoint at a meaningful phase boundary when architectural contracts stabilize, substantial implementation changes accumulate, or documentation is likely to have become materially inaccurate. - -Documentation checkpoints are advisory. Do not conduct a documentation audit or begin maintenance merely to make the recommendation. The user decides whether to continue engineering work or authorize documentation maintenance unless the transition was already approved. - -### Documentation audit - -Inspect the relevant implementation and documentation to identify inaccurate, obsolete, missing, duplicated, or unnecessary information. - -Report findings and recommended changes without modifying documentation. Distinguish documentation describing intended behavior from documentation incorrectly describing the current implementation. - -### Documentation maintenance - -Documentation should help a reader understand the current system, its intended direction, or the work required to reach that direction. - -Prefer updating and consolidating existing authoritative documents over creating new ones. Remove obsolete, duplicate, or superseded documentation when its useful information has been preserved. Do not expand the document collection merely to record intermediate work, reasoning, or implementation progress. - -Apply these rules to affected documents across `docs/` during authorized maintenance: - -- Describe current contracts in the present tense. Keep plans focused on the target state, unresolved work, prerequisites, ownership/dependency constraints, ordered steps, discovery boundaries and acceptance criteria. -- Keep one concise progress checklist for each active macro task in the plan that owns its details. Reconcile accumulated subtask progress during maintenance, retaining checked subtasks while the macro task remains open. Keep future macro tasks at roadmap detail until they become active; link to the owning checklist instead of maintaining duplicate progress lists. -- Remove completed macro-task narratives and superseded alternatives. Document resulting durable behavior in its authoritative subject document, and link to it instead of duplicating contracts or source inventories. -- Preserve unique requirements, decisions and rationale that still guide implementation. Before shortening or deleting a plan, verify that each remains available in the surviving text or a linked authoritative document. Do not replace relevant detail with a vague summary or a pointer to Git alone. -- Git commits and diffs hold implementation history. A brief decision/history summary with specific Git references is useful when it explains the current direction or saves a future reader substantial reconstruction; do not reproduce the task-by-task journal. -- Do not append routine successful build/test/check reports, pass counts, transcripts or toolchain snapshots unless explicitly requested. Retain reusable validation procedures and meaningful coverage limits. Distinguish source completion from executable acceptance; a successful exit with skipped or unselected checks does not establish the missing coverage. -- Record unresolved failures, unavailable prerequisites and environment/compatibility limits only to the extent they affect future work, with the required next action. Remove resolved blockers unless they describe an enduring constraint. -- Reconcile affected plans and current-state documentation against accumulated completed work. When a macro task completes, remove its task section and checklist after preserving any reusable procedure, durable contract or still-relevant decision rationale and repairing inbound links. Delete a one-purpose plan when no unresolved work remains. Do not move its execution log elsewhere merely to empty the plan. - -Keep a paragraph when it explains how something works, how it should work, how to get there, or a decision/constraint needed to do so correctly. Concision must preserve that information. - -## Git - -For commits created by the agent, use: - -- `user.name = cppcooper` -- `user.email = cppcooper@users.noreply.github.com` - -Apply this identity per Git command using `git -c`. Do not modify the user's global Git configuration or the repository's local Git configuration. - -Commit messages must follow the project's convention and begin with a third-person singular present-tense verb, such as: - -- `Adds` -- `Updates` -- `Revises` -- `Deletes` -- `Fixes` -- `Requires` - -Use detailed commit messages appropriate to the completed development unit. - -Preserve the individual commit series. Do not squash, amend, or otherwise rewrite completed commits unless explicitly requested. - -Do not generate mailbox patches unless explicitly requested. - -## Cheryl conventions - -- Follow the repository's existing style and `.clang-format`. -- When organizing class declarations, place data members above methods. -- Keep unit-test names short and intuitive rather than long and descriptive. -- Tests must exercise behavior without intentionally emitting compiler warnings in normal builds. Consume results when they matter to the assertion; explicitly discard irrelevant `[[nodiscard]]` results with `static_cast(...)`, including inside exception assertions. Apply the same warning discipline to other intentional diagnostic triggers. -- Test compiler diagnostics themselves through isolated probes that capture and assert the expected diagnostic. Keep any required diagnostic suppression narrowly scoped to the tested construct and compiler; do not remove API attributes or disable warnings across a target/project to accommodate a test. -- Do not use "no callers" or "few callers" as grounds for removing an interface. Evaluate interfaces according to their architectural purpose and contract. +# Cheryl Engine agent instructions + +This file is the entry point for repository instructions. Read the +[shared work rules](docs/for-agents/workflow.md) for every task, then the focused +guides that apply. Linked instructions are part of this instruction set. Read +applicable nested `AGENTS.md` files as well; `docs/AGENTS.md` routes documentation +work, including API comments outside `docs/`. + +## Essential safeguards + +- Work within the user's authorized scope and progression. Existing authorization + persists for relevant fixes and reruns; do not request it again unnecessarily. +- Inspect Git status and relevant diffs before edits. Protect unfinished user + work, active runs, outputs, build configurations and caches. Do not extend + unfinished work as a stable foundation without resolving that boundary. +- Follow the exact [operation permissions](docs/for-agents/testing.md#verification-and-resource-use) + before builds, native tests, full documentation generation, downloads, + installations, devices or external services. Available tools do not grant permission. +- Push only when explicitly requested. Follow the + [Git guide](docs/for-agents/git.md) for identity, commits and branch integration. +- Keep independent work objectives separate. Related cleanup, general documentation + changes and broader verification need their own scope; necessary implementation + investigation and directly related test maintenance belong to the requested fix. + +## Instruction index + +| Work | Read | +| --- | --- | +| Every task: scope, source authority, progression and shared-tree safety | [Shared work rules](docs/for-agents/workflow.md) | +| Review a problem, investigate behavior or develop a plan | [Investigation and planning](docs/for-agents/investigation-and-planning.md) | +| Design architecture, interfaces or ownership boundaries | [Architecture and design](docs/for-agents/architecture.md) | +| Implement a design, fix behavior, refactor or change tooling/SDK/CI | [Implementation](docs/for-agents/implementation.md) | +| Design, implement or run tests; verify results; use Windows tooling | [Testing and verification](docs/for-agents/testing.md) | +| Maintain the pending testing/QA queue | [Testing-request maintenance](docs/for-agents/testing-requests.md) | +| Use Git, commit, branch or consolidate into `fixes-develop` | [Git](docs/for-agents/git.md), including the established squash strategy | +| Audit, create, improve, consolidate or retire documentation | [Documentation workflow](docs/for-agents/documentation.md) | +| Document API types, fields, methods and templates | [API documentation](docs/for-agents/api-documentation.md), with the documentation workflow | +| Build/check the local documentation or diagnose stale output | [Documentation generation](docs/for-agents/documentation-building.md) | +| Maintain or extend these instruction sets | [Agent instruction library](docs/for-agents/README.md) | + +Choose guides by the work actually being done. A guide explains how to perform +its mode; it does not authorize starting an independent objective. Follow the +existing subject owner linked from that guide instead of loading the entire +library or planning directory. diff --git a/README.md b/README.md index 31725b13..dd582f9b 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ with `std::format` support. The default root configuration also requires Python 3.11 or newer for documentation targets and hwloc 2.5+ (2.x) for worker support. Set `CHERYL_BUILD_DOCS=OFF` or `CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC=OFF` to omit the corresponding requirement. Install the selected modules' -[system dependencies](docs/development/building.md#system-dependencies). +[system dependencies](docs/site/development/building.md#system-dependencies). Linux builds use ordinary shared development packages. Windows requires Windows 10 version 1903+ and the SDK's ICU C API; prepare its external shared dependencies -using the [dependency helper](docs/development/building.md#dependency-and-toolchain-settings). +using the [dependency helper](docs/site/development/building.md#dependency-and-toolchain-settings). ```sh git clone https://github.com/cppcooper/cheryl-engine.git @@ -79,7 +79,7 @@ After changing branches or updating the checkout, restore its pinned dependencie ) ``` -The [build guide](docs/development/building.md) lists configuration options, +The [build guide](docs/site/development/building.md) lists configuration options, dependency settings, Debug/Release behavior and common setup failures. Existing build directories retain cached options; reload CMake after changing selections. @@ -102,15 +102,15 @@ the public aliases to inherit headers and dependencies. | `module_ui_rmlui` | `Cheryl::UI::RmlUi` | RML/RCSS documents and retained scene bridge. | | `module_audio_miniaudio` | `Cheryl::Audio::Miniaudio` | Audio decoding, mixing, native output and streaming. | | `demo` | — | Example native application. | -| `first-game` | — | Opt-in maintained game from the [walkthrough](docs/getting-started/first-game.md), with its own assets. | -| `tutorial-game` | — | Opt-in Player/Enemy, direct text score and TGUI controls example supporting the [tutorials](docs/getting-started/create-a/index.md) and [How-to guides](docs/getting-started/how-to/index.md). | +| `first-game` | — | Opt-in maintained game from the [walkthrough](docs/site/getting-started/first-game.md), with its own assets. | +| `tutorial-game` | — | Opt-in Controller/Character, joining players, patrol and TGUI HUD example supporting the [tutorials](docs/site/getting-started/create-a/index.md) and [task index](docs/site/getting-started/how-to/index.md). | | `docs`, `docs-clean`, `docs-tools` | — | Optional incremental generation, full regeneration and shared documentation tool setup; also available through the docs-only CMake project. | | `cheryl-sdk` | — | Builds selected installable libraries when `CHERYL_BUILD_SDK=ON`. | See the [module index](projects/modules/README.md) for each owner's API and the -[consumption guide](docs/development/consuming-engine.md) for application CMake +[consumption guide](docs/site/development/consuming-engine.md) for application CMake composition. Applications can also use an installed prebuilt SDK through -[`find_package(Cheryl 4 CONFIG REQUIRED)`](docs/development/consuming-engine.md#prebuilt-development-sdks). +[`find_package(Cheryl 4 CONFIG REQUIRED)`](docs/site/development/consuming-engine.md#prebuilt-development-sdks). ## Tests @@ -127,14 +127,14 @@ With tests enabled, build and run the combined GoogleTest runner for selected ow ) ``` -The [test guide](docs/development/testing.md) explains focused runners, aggregates, +The [test guide](docs/site/development/testing.md) explains focused runners, aggregates, CTest selection, consumers and environment opt-ins. Native graphics and real-font checks need their stated prerequisites; skipped cases leave that coverage pending. The [testing queue](docs/testing-requests.md) contains pending automated checks and QA. ## CI and development SDKs -[GitHub CI](docs/development/ci.md) selects Linux +[GitHub CI](docs/site/development/ci.md) selects Linux GCC/Clang and Windows MSVC builds, focused tests and installed SDK consumers. Successful main-push validation collects prebuilt development SDKs as workflow artifacts. They contain compiled libraries, public/inline/template headers, CMake @@ -157,7 +157,7 @@ root CMake file selects owners; `cmake/` supplies shared metadata and compositio helpers. `extern/` holds pinned dependencies and the `assets/` submodule holds definitions, textures, shader recipes, UI documents and font inputs. Initialize it alongside the dependencies; optional package images still follow the -[catalog](docs/assets/catalog.md). See [module authoring](docs/development/modules.md) +[catalog](docs/site/assets/catalog.md). See [module authoring](docs/site/development/modules.md) for the steps required to add an owner. ## Documentation and development @@ -165,33 +165,34 @@ for the steps required to add an owner. From the checkout root, run `./generate-docs.sh` to update the local manual and API reference. Add `--clean` for full regeneration, `--extract-only` for Doxygen XML, or `--help` for output and tool-selection options. The -[generation guide](docs/development/documentation.md) also covers Python and CMake +[generation guide](docs/site/development/documentation.md) also covers Python and CMake entry points and hosted publication. +The generated entry point is `docs/site/generation/generated/html/index.html`. | Topic | Documentation | |----------------------------------------| --- | | Complete documentation index | [All guides and system references](docs/README.md) | -| Engine architecture | [Engine assembly and backend boundaries](docs/architecture/assembling-engine.md), [frame lifecycle](docs/runtime/runtime-frame-boundary.md) and [module roles](projects/modules/README.md) | -| Engine systems | [Purpose, ownership, execution and integration by system](docs/engine-systems/index.md) | -| **CMake configuration** and **builds** | [Options and defaults](docs/development/building.md#configuration-options), [build configurations](docs/development/building.md#build-configurations) and [engine-only builds](docs/development/building.md#engine-only-configuration) | -| Dependencies | [SDK selection and toolchain settings](docs/development/building.md#dependency-and-toolchain-settings), [system requirements](docs/development/building.md#system-dependencies) and [public headers and link requirements](docs/development/consuming-engine.md#dependencies-and-headers) | -| Targets and artifacts | [Module targets and aliases](projects/modules/README.md), [test targets](docs/development/testing.md#runner-selection) and [artifact name definitions](cmake/CherylOutputs.cmake) | -| Writing a game | [Project setup](docs/getting-started/project-setup.md), [First-game walkthrough](docs/getting-started/first-game.md), [Player/Enemy/HUD](docs/getting-started/create-a/index.md), [How-to guides](docs/getting-started/how-to/index.md) and [consumption reference](docs/development/consuming-engine.md) | -| Documentation generation and hosting | [Local generation, shared tools and versioned Read the Docs setup](docs/development/documentation.md), [generated API entry points](docs/api/index.md) | -| Adding a module | [Module authoring](docs/development/modules.md#add-a-module), [standalone composition](docs/development/modules.md#standalone-modules) and [UI adapters](docs/development/ui-adapters.md) | -| Platform and graphics APIs | [Native GLFW](projects/modules/platform/native-glfw/README.md) and [OpenGL](projects/modules/graphics/opengl/README.md) | -| UI APIs | [TGUI](projects/modules/ui/tgui/README.md) and [RmlUi](projects/modules/ui/rmlui/README.md) | -| Audio APIs | [Engine audio contracts](docs/runtime/audio.md) and [miniaudio integration](projects/modules/audio/miniaudio/README.md) | -| Input, timing and concurrency | [Input state and events](docs/runtime/input-state-model.md), [simulation timing](docs/runtime/simulation-timing.md) and [thread dispatch](docs/runtime/thread-dispatch.md) | -| Assets and text | [Asset loading](docs/assets/asset-loading.md), [manifest format](docs/assets/asset-manifests.md) and [Unicode text layout](docs/assets/text-layout.md) | -| Rendering and resources | [Pipelines and materials](docs/rendering/pipelines-and-materials.md), [CPU memory lifetime](docs/resources/resource-lifetime.md) and [resource residency](docs/resources/resource-residency.md) | -| Logging and crash handling | [Compile-time logging policy](docs/runtime/logging.md#compile-policy), [runtime logging](docs/runtime/logging.md#runtime-settings-and-ownership) and [crash bootstrap and exception traces](docs/development/consuming-engine.md#crash-and-exception-traces) | -| Native output terminal | [Linux module, activation and viewer lifetime](projects/modules/platform/debug-terminal-linux/README.md) | -| Demo | [Controls](projects/apps/demo/README.md#controls), [command-line options](projects/apps/demo/README.md#command-line-options) and [asset packages](docs/assets/catalog.md) | -| Automated tests | [Runners and CTest selection](docs/development/testing.md), [acceptance checks](docs/development/testing.md#acceptance-and-consumers) and [logging acceptance](docs/development/logging-acceptance.md) | -| Consumers and header probes | [Selection and targets](docs/development/testing.md#acceptance-and-consumers) and [independent consumer builds](docs/development/consuming-engine.md#independent-consumers-and-header-probes) | -| Development SDKs and CI | [SDK production](docs/development/building.md#development-sdk-builds), [prebuilt consumption](docs/development/consuming-engine.md#prebuilt-development-sdks) and [hosted CI matrix](docs/development/ci.md) | -| Architecture validation and QA | [Validation procedures](docs/development/architecture-validation.md), [desktop checks](docs/development/native-desktop-checks.md) and [pending testing requests](docs/testing-requests.md) | -| Troubleshooting | [Build and dependency issues](docs/development/building.md#troubleshooting) and [compiler discovery and standard headers](docs/development/consuming-engine.md#standard-headers-in-an-existing-build) | -| Contributing | [Development workflow](docs/development/contributing.md), [C++ style](docs/development/code-style.md) and [agent instructions](AGENTS.md) | -| Planning | [Planning catalogue](docs/planning/README.md) and [development roadmap](docs/planning/develop-review-and-development-plan.md) | +| Engine systems and integration | [System relationships](docs/site/engine-systems/index.md), [application integration](docs/site/development/consuming-engine.md), [frame lifecycle](docs/site/runtime/runtime-frame-boundary.md) and [module selection](docs/site/modules/index.md) | +| Engine systems | [Purpose, ownership, execution and integration by system](docs/site/engine-systems/index.md) | +| **CMake configuration** and **builds** | [Options and defaults](docs/site/development/building.md#configuration-options), [build configurations](docs/site/development/building.md#build-configurations) and [engine-only builds](docs/site/development/building.md#engine-only-configuration) | +| Dependencies | [SDK selection and toolchain settings](docs/site/development/building.md#dependency-and-toolchain-settings), [system requirements](docs/site/development/building.md#system-dependencies) and [public headers and link requirements](docs/site/development/consuming-engine.md#dependencies-and-headers) | +| Targets and artifacts | [Module targets and aliases](projects/modules/README.md), [test targets](docs/site/development/testing.md#runner-selection) and [artifact name definitions](cmake/CherylOutputs.cmake) | +| Writing a game | [Project setup](docs/site/getting-started/project-setup.md), [Tutorials](docs/site/getting-started/create-a/index.md), [Task index](docs/site/getting-started/how-to/index.md) and [Guides by topic](docs/site/guides/index.md) | +| Documentation generation and hosting | [Local generation, shared tools and versioned Read the Docs setup](docs/site/development/documentation.md), [generated API entry points](docs/site/api/index.md) | +| Adding a module | [Module authoring](docs/site/development/modules.md#add-a-module), [standalone composition](docs/site/development/modules.md#standalone-modules) and [UI adapters](docs/site/development/ui-adapters.md) | +| Platform and graphics APIs | [Native GLFW](docs/site/modules/native-glfw.md) and [OpenGL](docs/site/modules/opengl.md) | +| UI APIs | [TGUI](docs/site/modules/tgui.md) and [RmlUi](docs/site/modules/rmlui.md) | +| Audio APIs | [Engine audio contracts](docs/site/runtime/audio.md) and [miniaudio integration](docs/site/modules/miniaudio.md) | +| Input, timing and concurrency | [Input state and events](docs/site/runtime/input-state-model.md), [simulation timing](docs/site/runtime/simulation-timing.md) and [thread dispatch](docs/site/runtime/thread-dispatch.md) | +| Assets and text | [Asset loading](docs/site/assets/asset-loading.md), [manifest format](docs/site/assets/asset-manifests.md) and [Unicode text layout](docs/site/assets/text-layout.md) | +| Rendering and resources | [Pipelines and materials](docs/site/rendering/pipelines-and-materials.md), [CPU memory lifetime](docs/site/resources/resource-lifetime.md) and [resource residency](docs/site/resources/resource-residency.md) | +| Logging and crash handling | [Compile-time logging policy](docs/site/runtime/logging.md#compile-policy), [runtime logging](docs/site/runtime/logging.md#runtime-settings-and-ownership) and [crash bootstrap and exception traces](docs/site/development/consuming-engine.md#crash-and-exception-traces) | +| Native output terminal | [Linux module, activation and viewer lifetime](docs/site/modules/debug-terminal-linux.md) | +| Demo | [Controls](projects/apps/demo/README.md#controls), [command-line options](projects/apps/demo/README.md#command-line-options) and [asset packages](docs/site/assets/catalog.md) | +| Automated tests | [Runners and CTest selection](docs/site/development/testing.md), [acceptance checks](docs/site/development/testing.md#acceptance-and-consumers) and [logging acceptance](docs/site/development/logging-acceptance.md) | +| Consumers and header probes | [Selection and targets](docs/site/development/testing.md#acceptance-and-consumers) and [independent consumer builds](docs/site/development/architecture-validation.md#independent-consumers-and-header-probes) | +| Development SDKs and CI | [SDK production](docs/site/development/building.md#development-sdk-builds), [prebuilt consumption](docs/site/development/consuming-engine.md#prebuilt-development-sdks) and [hosted CI matrix](docs/site/development/ci.md) | +| Architecture validation and QA | [Validation procedures](docs/site/development/architecture-validation.md), [desktop checks](docs/site/development/native-desktop-checks.md) and [pending testing requests](docs/testing-requests.md) | +| Troubleshooting | [Build and dependency issues](docs/site/development/building.md#troubleshooting) and [compiler discovery and standard headers](docs/site/development/building.md#standard-headers-in-an-existing-build) | +| Contributing | [Development workflow](docs/site/development/contributing.md), [C++ style](docs/site/development/code-style.md) and [agent instructions](AGENTS.md) | +| Planning | [Status and selected work](docs/STATUS.md) and [planning catalogue](docs/planning/README.md) | diff --git a/cmake/CherylDocumentation.cmake b/cmake/CherylDocumentation.cmake index 76a20f9b..3adeb6e0 100644 --- a/cmake/CherylDocumentation.cmake +++ b/cmake/CherylDocumentation.cmake @@ -19,19 +19,19 @@ function(cheryl_add_documentation_targets repository_root) list(APPEND tool_arguments --force-pinned) endif() add_custom_target(docs-tools - COMMAND "${CHERYL_DOCS_PYTHON}" "${repository_root}/docs/generation/bootstrap.py" setup ${tool_arguments} + COMMAND "${CHERYL_DOCS_PYTHON}" "${repository_root}/docs/site/generation/bootstrap.py" setup ${tool_arguments} WORKING_DIRECTORY "${repository_root}" COMMENT "Preparing shared documentation tools" USES_TERMINAL VERBATIM) add_custom_target(docs - COMMAND "${CHERYL_DOCS_PYTHON}" "${repository_root}/docs/generation/bootstrap.py" exec ${tool_arguments} - -- python "${repository_root}/docs/generation/build.py" + COMMAND "${CHERYL_DOCS_PYTHON}" "${repository_root}/docs/site/generation/bootstrap.py" exec ${tool_arguments} + -- python "${repository_root}/docs/site/generation/build.py" WORKING_DIRECTORY "${repository_root}" COMMENT "Updating Cheryl documentation" USES_TERMINAL VERBATIM) add_custom_target(docs-clean - COMMAND "${CHERYL_DOCS_PYTHON}" "${repository_root}/docs/generation/bootstrap.py" exec ${tool_arguments} - -- python "${repository_root}/docs/generation/build.py" --clean + COMMAND "${CHERYL_DOCS_PYTHON}" "${repository_root}/docs/site/generation/bootstrap.py" exec ${tool_arguments} + -- python "${repository_root}/docs/site/generation/build.py" --clean WORKING_DIRECTORY "${repository_root}" COMMENT "Regenerating Cheryl documentation from scratch" USES_TERMINAL VERBATIM) diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 00000000..5fde1111 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,21 @@ +# Documentation work + +The root [AGENTS.md](../AGENTS.md) indexes the repository instruction sets. +Read the [shared workflow](for-agents/workflow.md) and the guides for the current +mode. Those rules also apply when documentation is written in source headers. + +For authorized documentation auditing or maintenance: + +1. Follow [Documentation workflow](for-agents/documentation.md) for ownership, + evidence, writing, usefulness review and maintenance policy. +2. Also read [API documentation](for-agents/api-documentation.md) for source + comments and [Documentation generation](for-agents/documentation-building.md) + when generating or inspecting the site. +3. Read the affected subject's canonical guide and source owner; follow links + that answer a question needed for this task. + +The [documentation index](README.md) maps published and repository-only material. +The [published hierarchy](site/index.md) owns site navigation. The +[tooling guide](site/development/documentation.md) owns generation commands and +output/cache behavior. For changes to the instruction sets themselves, also use +[Agent instruction library](for-agents/README.md#maintain-this-library). diff --git a/docs/README.md b/docs/README.md index 235b55bd..21cc5624 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,100 +1,89 @@ # Cheryl Engine documentation -Choose an entry point for the work you are doing: +Choose the entry point for your work: | Task | Start here | | --- | --- | -| Look up a declaration | [C++ API](api/index.md) | -| Write a game | [Project setup](getting-started/project-setup.md), then [First Game](getting-started/first-game.md) | -| Choose a system or public type | [Engine Systems](engine-systems/index.md) | -| Perform an operation | [Detailed Guides](guides/index.md) | -| Select a concrete integration | [Modules](modules/index.md) | -| Connect, extend or replace components | [Architecture](architecture/index.md) | -| Configure, build or troubleshoot | [Setup](../README.md#setup) and [build reference](development/building.md) | -| Run checks | [Test selection](development/testing.md) and [pending checks and QA](testing-requests.md) | -| Check current capabilities or pick up planned work | [Status inventory](STATUS.md) and [planning catalogue](planning/README.md) | - -## API Reference - -- [Generated C++ symbols, entry points and migration](api/index.md) - -## Getting Started - -- [Project setup, CMake, and source/SDK choices](getting-started/project-setup.md) -- [First game: startup, architecture, sprites, and text](getting-started/first-game.md) -- [Create a Player, Enemy or HUD](getting-started/create-a/index.md) -- [How to: focused game-development tasks](getting-started/how-to/index.md) -- [Maintained Player/Enemy, direct text and TGUI example](../projects/apps/tutorial-game/README.md) - -## Engine Systems - -[Start with the ownership and execution overview](engine-systems/index.md), then -read a system's purpose, type/operation tables, important enums, relationships, -contracts and lifecycle. Table entries link to the detailed guides and API: - -- [Runtime](engine-systems/runtime.md), [Display](engine-systems/display.md) and [Input](engine-systems/input.md) -- [Assets](engine-systems/assets.md), [Resources](engine-systems/resources.md), [Rendering](engine-systems/rendering.md) and [Text](engine-systems/text.md) -- [GUI](engine-systems/gui.md) and [Audio](engine-systems/audio.md) -- [Events](engine-systems/events.md), [Dispatch](engine-systems/dispatch.md) and [Workers](engine-systems/workers.md) -- [Memory](engine-systems/memory.md), [State](engine-systems/state.md), [Logging](engine-systems/logging.md), [Diagnostics](engine-systems/diagnostics.md) and [Utilities](engine-systems/utilities.md) - -## Detailed Guides - -- [Assets](assets/index.md): loading, manifests, animation and tile selection. -- [Text](text/index.md): font selection, shaping, encoding and legacy compatibility. -- [Rendering and Resources](rendering/index.md): frames, materials, sampling and retirement. -- [Input](input/index.md): actions, records, capture and focus. -- [GUI](gui/index.md): interface construction, publication and cleanup. -- [Audio](audio/index.md): clips, voices, streams and output lifetime. -- [Runtime and Concurrency](runtime/index.md): lifecycle, timing, windows, dispatch, events and workers. -- [Memory, State and Utilities](utilities/index.md): object allocation, state choices and CPU helpers. -- [Diagnostics](diagnostics/index.md): logging, observations and failure reporting. - -## Modules - -- [Selection and roles](modules/index.md) -- [Source/SDK consumption and startup](development/consuming-engine.md) -- [Adapter contracts required by Engine and optional integrations](architecture/interfaces-adapters.md) -- [Startup](modules/startup.md) and [Workers](modules/workers.md) -- [Native GLFW](modules/native-glfw.md) and [OpenGL](modules/opengl.md) -- [TGUI](modules/tgui.md) and [RmlUi](modules/rmlui.md) -- [miniaudio](modules/miniaudio.md) -- [Linux Debug terminal](modules/debug-terminal-linux.md) - -The module pages publish the owner guides; the original owner READMEs remain -available beside their source and in the [repository module index](../projects/modules/README.md). -[Module authoring](development/modules.md) and -[UI adapter authoring](development/ui-adapters.md) belong to Development. - -## Architecture - -- [Interfaces and Adapters](architecture/interfaces-adapters.md): contracts, implementations and replacement dependencies. -- [Assembling the Engine](architecture/assembling-engine.md): facades, factories and compatible context composition. -- [Ownership and Cleanup](architecture/ownership-cleanup.md): borrowed objects, retained handles, leases and shutdown. -- [Threads and Data Flow](architecture/threads-data-flow.md): execution owners, delivery choices and publication. - -## Development - -- [Generate documentation](development/documentation.md) and [publish versioned documentation](development/publishing-documentation.md) -- [Pending automated and QA testing requests](testing-requests.md) -- [Build and produce SDKs](development/building.md) and [operate CI and SDK collection](development/ci.md) -- [Test selection](development/testing.md) -- [Contributing](development/contributing.md) -- [C++ code style](development/code-style.md) -- [Architecture validation procedures](development/architecture-validation.md) -- [Logging acceptance](development/logging-acceptance.md) -- [Engine and integration modules](development/modules.md) -- [RmlUi placeholder investigation and contribution guidance](external-work/rmlui-placeholder-issue.md) -- [Writing a UI adapter](development/ui-adapters.md) -- [Neutral UI consumer checks](development/architecture-validation.md#neutral-ui-consumer) -- [Desktop smoke checks](development/native-desktop-checks.md) - -## Planning - -- [Planning catalogue](planning/README.md): horizons, owning plans and prerequisites. -- [Development roadmap](planning/develop-review-and-development-plan.md): active sequencing. - -Keep new documents in the matching topic folder and add them to this index. Use -lowercase, hyphenated filenames, with `README.md` reserved for documentation -indexes. Use relative links between documents and to source files so navigation -works from each document's location. +| Look up a declaration | [C++ API](site/api/index.md) | +| Set up a game project | [Project setup](site/getting-started/project-setup.md) | +| Follow a complete example | [Tutorials](site/getting-started/create-a/index.md): First Game, Controller, Character and TGUI HUD | +| Find an operation | [Task index](site/getting-started/how-to/index.md) | +| Browse a subject | [Guides by topic](site/guides/index.md) | +| Understand system responsibilities and public types | [Engine Systems](site/engine-systems/index.md) | +| Change or extend Cheryl | [Contributing](site/development/contributing.md) and [Module authoring](site/development/modules.md) | +| Check capabilities or planned work | [Status inventory](STATUS.md) and [planning catalogue](planning/README.md) | + +## Source layout + +| Location | Owner and audience | +| --- | --- | +| `site/` | Canonical sources for the published manual and module references. | +| `site/generation/` | Generator, configuration, theme assets and tooling checks. Local HTML is written to `site/generation/generated/html/`. | +| `for-agents/` | Focused agent work instructions, indexed by root `AGENTS.md`: Git, investigation, design, implementation, testing and documentation. | +| `planning/`, `STATUS.md`, `testing-requests.md` | Repository-only plans, investigation follow-up, status and pending acceptance. | + +The generator reads `site/`. Keep published prose there. Module `README.md` +files beside the code are relative symlinks to their canonical module pages; +edit and preview those pages through `site/modules/`. Repository-only pages use +ordinary relative links to the site sources. The site converts links to +repository-only documents into versioned source links. + +## Game Development + +[Project setup](site/getting-started/project-setup.md) provides the first working +source/SDK configuration. [Choose modules](site/modules/index.md) maps capabilities to +implementations; [Source and SDK integration](site/development/consuming-engine.md) +owns detailed linking, context assembly and deployment. + +The [tutorial sequence](site/getting-started/create-a/index.md) starts with First Game, +then composes a Controller, Character and TGUI HUD. The +[maintained tutorial application](../projects/apps/tutorial-game/README.md) +implements the class-based lessons. + +The [Task index](site/getting-started/how-to/index.md) links to instructions directly. +Browse the same procedures and references by subject through: + +- [Assets and animation](site/assets/index.md) +- [Rendering and resources](site/rendering/index.md) +- [Input](site/input/index.md) +- [Text and fonts](site/text/index.md) +- [GUI](site/gui/index.md) +- [Audio](site/audio/index.md) +- [Runtime and concurrency](site/runtime/index.md), including ownership, cleanup and work delivery +- [Memory, state and utilities](site/utilities/index.md) +- [Logging and diagnostics](site/diagnostics/index.md) + +[Engine Systems](site/engine-systems/index.md) completes the category with a map of +responsibilities, public types, ownership and relationships. Concrete module +references belong to the topic they implement. Their canonical prose lives in +`site/modules/`; the +[repository module catalogue](../projects/modules/README.md) maps their source owners. + +## Engine Development + +- [Contributing](site/development/contributing.md) and [C++ code style](site/development/code-style.md) +- [Build the engine and SDKs](site/development/building.md) +- [Module authoring](site/development/modules.md), [Integration contracts](site/architecture/interfaces-adapters.md) and [UI adapter authoring](site/development/ui-adapters.md) +- [Testing and validation](site/development/testing.md), [Component and SDK checks](site/development/architecture-validation.md), [Logging checks](site/development/logging-acceptance.md) and [Desktop and controller checks](site/development/native-desktop-checks.md) +- [Documentation tooling](site/development/documentation.md) and [Publishing](site/development/publishing-documentation.md) +- [CI and SDK artifacts](site/development/ci.md) + +[Pending checks and QA](testing-requests.md) records outstanding acceptance. +The [planning catalogue](planning/README.md) links active work and its prerequisites. + +## Documentation organization + +[Agent instructions](../AGENTS.md#instruction-index) route work through the +focused [instruction library](for-agents/README.md), including page outlines, +writing and precision checks, API comments and documentation generation. These +instructions are repository-only. + +[The published navigation](site/index.md) and each parent page's toctree own the page +hierarchy. Give a page one navigation parent; use ordinary links for other routes +to its instructions. Keep task groups in the Task index's local contents and +implementation-specific contracts in their owning references. + +Published paths can remain stable when a page's navigation home changes. Preserve +useful old anchors when consolidating content. Use relative links to documents +and source files. Keep published pages in the relevant topic folder under `site/`, with +lowercase, hyphenated filenames and `README.md` reserved for repository indexes. diff --git a/docs/STATUS.md b/docs/STATUS.md index 14ac141f..68ac4edc 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -14,7 +14,7 @@ new build/test/native results. Independent Engine-only graph inspection and the Linux terminal configuration matrix are accepted. Hosted Linux/Windows builds and relocated SDK consumers are -accepted within the [CI coverage limits](development/ci.md). +accepted within the [CI coverage limits](site/development/ci.md). The first main-push SDK collection remains [TR20](testing-requests.md#tr20-automated-main-push-sdk-collection); terminal desktop and demo material/artwork observations remain pending. These acceptance updates do @@ -25,6 +25,11 @@ reorder-safe sorting, color emoji, an alternative audio SDK and the in-game cons Deferred work includes absent capabilities and Windows worker affinity in phase 4; this scheduling update does not change the implementation review baseline above. +**Planning consolidation:** 2026-10-18 — This inventory owns selected work and +deferred review order. The [planning catalogue](planning/README.md) routes to +subject prerequisites and unfinished checklists. This consolidation does not +extend implementation or executable acceptance. + **Classification:** **Partial** = implemented functionality with a selected integration gap; **Planned** = selected work awaiting investigation, design or implementation; **Deferred** = deliberately postponed, unselected, or blocked work, including capabilities with no complete implementation; **Implemented** = substantive source implementation exists, but does not imply a verified build or universal platform coverage. The following **four classification sections** comprise 146 distinct entries. Links are relative to `docs/STATUS.md`, so they follow the branch in which the document is viewed. @@ -37,20 +42,21 @@ No current entries in this classification. ## 2. Planned -The [roadmap](planning/develop-review-and-development-plan.md#selected-planned-work) -owns these selected tasks. Their prerequisites and design checkpoints remain; -selection does not authorize implementation or establish executable acceptance. +These tasks are selected for investigation and design. Each feature needs settled +scope and acceptance before implementation. Selection does not promote deferred +general batching or other integrations, authorize implementation or establish +executable acceptance. Subject plans own the prerequisites and design checkpoints. | Feature / integration | Status | Description / scope | Investigate | | --- | --- | --- | --- | -| GoogleTest submodule review | Planned | Review upstream history since the pinned revision, assess test/CMake/toolchain compatibility and decide whether to update the submodule; validate affected test owners if an update is selected. | [dependency reviews](planning/develop-review-and-development-plan.md#dependency-reviews) | -| nlohmann/json submodule review | Planned | Review upstream history since the pinned revision and assess parsing, serialization, configuration and package compatibility before deciding whether to update the JSON submodule. | [dependency reviews](planning/develop-review-and-development-plan.md#dependency-reviews) | -| spdlog submodule review | Planned | Review upstream history since the pinned revision and assess logging/sink behavior, formatting dependencies, exported targets and toolchain compatibility before deciding whether to update the submodule. | [dependency reviews](planning/develop-review-and-development-plan.md#dependency-reviews) | -| RmlUi rendering effects | Planned | Core document/font/input/clipping and retained-upload integration exists. Plan masks, transforms, layers, filters, custom shaders and repeating texture coordinates, including any neutral rendering-contract and graphics-backend changes needed to support them. | [UI extension plan](planning/long-term/README.md#ui-adapters) | +| GoogleTest submodule review | Planned | Review upstream history since the pinned revision, assess test/CMake/toolchain compatibility and decide whether to update the submodule; validate affected test owners if an update is selected. | [dependency reviews](planning/README.md#dependency-reviews) | +| nlohmann/json submodule review | Planned | Review upstream history since the pinned revision and assess parsing, serialization, configuration and package compatibility before deciding whether to update the JSON submodule. | [dependency reviews](planning/README.md#dependency-reviews) | +| spdlog submodule review | Planned | Review upstream history since the pinned revision and assess logging/sink behavior, formatting dependencies, exported targets and toolchain compatibility before deciding whether to update the submodule. | [dependency reviews](planning/README.md#dependency-reviews) | +| RmlUi rendering effects | Planned | Core document/font/input/clipping and retained-upload integration exists. Plan masks, transforms, layers, filters, custom shaders and repeating texture coordinates, including any neutral rendering-contract and graphics-backend changes needed to support them. | [plan](planning/long-term/README.md#ui-adapters) | | Reorder-safe render sorting | Planned | Sorting is not performed; identify explicitly reorderable regions and preserve required authored draw order, clipping and retained generations before implementing a measured policy. | [sorting scope](planning/mid-term/README.md#reorder-safe-render-sorting) | | Color emoji glyphs | Planned | Specify accepted color-font formats, RGBA page handling, material semantics and multi-codepoint fallback beyond the accepted initial grayscale scope. | [emoji scope](planning/mid-term/README.md#color-emoji) | -| Alternative audio SDK (e.g., FMOD) | Planned · SDK selection pending | Select and assess an alternative SDK/module against the existing audio contracts, including licensing, deployment, coexistence and any additional authoring requirements. | [audio scope](planning/long-term/README.md#other-engine-extensions) | -| In-game developer console | Planned | Define and implement an engine-owned graphical command/output panel with history, scrolling and input routing, independently of the optional native terminal. | [console scope](planning/long-term/README.md#other-engine-extensions) | +| Alternative audio SDK (e.g., FMOD) | Planned · SDK selection pending | Select and assess an alternative SDK/module against the existing audio contracts, including licensing, deployment, coexistence and any additional authoring requirements. | [plan](planning/long-term/README.md#alternative-audio-sdk) | +| In-game developer console | Planned | Define and implement an engine-owned graphical command/output panel with history, scrolling and input routing, independently of the optional native terminal. | [plan](planning/long-term/README.md#graphical-developer-console) | *8 entries.* @@ -68,30 +74,30 @@ within each phase. | Feature / integration | Status | Description / scope | Investigate | | --- | --- | --- | --- | -| General render batching | Deferred · phase 1 · unselected extension | Arbitrary contiguous packet merging requires a separate measured design; opt-in positioned text runs are implemented without sorting or changing UI packet policy. | [batching scope](planning/mid-term/README.md#general-render-batching), [text runs](assets/text-layout.md#positioned-text-runs) | +| General render batching | Deferred · phase 1 · unselected extension | Arbitrary contiguous packet merging requires a separate measured design; opt-in positioned text runs are implemented without sorting or changing UI packet policy. | [batching scope](planning/mid-term/README.md#general-render-batching), [text runs](site/assets/text-layout.md#positioned-text-runs) | | Gainput HID backend | Deferred · phase 1 · blocked TR6 | Backend initialization, device identity, retained reports and lifecycle notifications remain unfinished. HID acceptance is deferred with backend work and the observation harness. | [HID plan](planning/long-term/README.md#deferred-hid-integration) | -| Manual collision checks (clipping) | Deferred · phase 1 | No engine-level manual overlap/intersection queries for detecting object clipping; specify shape, coordinate and boundary rules independently of physics simulation. | [collision scope](planning/long-term/README.md#other-engine-extensions) | +| Manual collision checks (clipping) | Deferred · phase 1 | No engine-level manual overlap/intersection queries for detecting object clipping; specify shape, coordinate and boundary rules independently of physics simulation. | [plan](planning/long-term/README.md#game-systems) | | Multiple active rendering windows | Deferred · phase 2 | A display can own several windows, but switching the selected rendering window rejects. Multiple active contexts/presentation and their ownership require a separately selected extension. | [window plan](planning/long-term/README.md#multiple-active-rendering-windows) | | Windows native acceptance | Deferred · phase 2 | Verify GLFW/Gainput input, resize/events, controller behavior and HID notifications on Windows; some guarded platform-specific code already exists. | [platform-acceptance.md](planning/long-term/platform-acceptance.md) | -| Alternative graphics APIs | Deferred · phase 2 | No selected Vulkan, Metal or Direct3D renderer; future modules should test existing neutral graphics/resource contracts. | [README.md](planning/long-term/README.md) | -| Adaptive timing adviser | Deferred · phase 2 | Potential profiling-driven timing recommendations without changing explicit simulation/input policy or silently retiming updates. | [README.md](planning/mid-term/README.md) | -| Steam Input | Deferred · phase 2 | Define controller ownership, device selection and native/Steam interaction; prove SDK-free input policies before transport. | [README.md](planning/long-term/README.md) | -| Multi-source input composition | Deferred · phase 2 | Merge native and optional provider contributions before publishing one logical action snapshot, with precise edge and focus semantics. | [README.md](planning/long-term/README.md) | -| Multiple native input owners | Deferred · phase 2 | Generalize beyond one Gainput-initialized owner and coordinate native notification-window lifetime where demanded by consumers. | [README.md](planning/long-term/README.md) | -| Additional UI toolkit modules | Deferred · phase 2 | Add another toolkit only for a demonstrated consumer requirement; preserve neutral Engine contracts and toolkit-owned widget APIs. | [README.md](planning/long-term/README.md) | -| Expanded manifest families | Deferred · phase 2 | Broader declarative asset formats remain long-term until a specific application requires their semantics. | [README.md](planning/long-term/README.md) | +| Alternative graphics APIs | Deferred · phase 2 | No selected Vulkan, Metal or Direct3D renderer; future modules should test existing neutral graphics/resource contracts. | [plan](planning/long-term/README.md#additional-graphics-backends) | +| Adaptive timing adviser | Deferred · phase 2 | Potential profiling-driven timing recommendations without changing explicit simulation/input policy or silently retiming updates. | [plan](planning/mid-term/README.md#measured-optimization) | +| Steam Input | Deferred · phase 2 | Define controller ownership, device selection and native/Steam interaction; prove SDK-free input policies before transport. | [plan](planning/long-term/README.md#steam-api-and-input-integration) | +| Multi-source input composition | Deferred · phase 2 | Merge native and optional provider contributions before publishing one logical action snapshot, with precise edge and focus semantics. | [plan](planning/long-term/README.md#policy-before-sdk-transport) | +| Multiple native input owners | Deferred · phase 2 | Generalize beyond one Gainput-initialized owner and coordinate native notification-window lifetime where demanded by consumers. | [plan](planning/long-term/README.md#broader-native-input-ownership) | +| Additional UI toolkit modules | Deferred · phase 2 | Add another toolkit only for a demonstrated consumer requirement; preserve neutral Engine contracts and toolkit-owned widget APIs. | [plan](planning/long-term/README.md#ui-adapters) | +| Expanded manifest families | Deferred · phase 2 | Broader declarative asset formats remain long-term until a specific application requires their semantics. | [plan](planning/README.md#topics-awaiting-plans) | | Entity-component system | Deferred · phase 2 | No built-in entity registry, components/archetypes or prescribed ECS; game logic architecture belongs to the consumer. | [abstract-game.h](../projects/engine/include/cheryl/core/game-framework/abstract-game.h) | | Tilemap/world system | Deferred · phase 2 | Autotile selection is provided, but tilemap storage, regions, streaming and world-level map management are not. | [tile-selection.h](../projects/engine/include/cheryl/assets/selection/tile-selection.h) | | Physics system | Deferred · phase 2 | No physics world, rigid-body simulation or built-in solver; this simulation scope is separate from manual collision queries. | [cheryl/](../projects/engine/include/cheryl/) | | Save-game serialization | Deferred · phase 2 | Manifest JSON parsing does not constitute an application save-state or serialization subsystem. | [resources/](../projects/engine/include/cheryl/core/resources/) | -| Text shaping/glyph optimization | Deferred · phase 3 | Profile paragraph shaping, glyph preparation and upload before adding caches or accelerated line fitting. | [README.md](planning/mid-term/README.md) | -| GPU memory budgets/eviction | Deferred · phase 3 | Resource handles and caches are implemented, but automatic residency budgets, eviction and GPU-memory policy are absent. | [resource-provider.h](../projects/engine/include/cheryl/assets/resources/resource-provider.h) | -| Independent provider cache domains | Deferred · phase 3 | Global asset managers currently use one active provider/loading-owner domain; parallel independent cache domains are unsupported. | [asset-mgr.h](../projects/engine/include/cheryl/templates/asset-mgr.h) | -| Monitor hotplug event service | Deferred · phase 3 | Monitor enumeration exists; an engine-level dynamic monitor-added/removed subscription and lifecycle contract is absent. | [display-system-interface.h](../projects/engine/include/cheryl/core/display/display-system-interface.h) | +| Text shaping/glyph optimization | Deferred · phase 3 | Profile paragraph shaping, glyph preparation and upload before adding caches or accelerated line fitting. | [plan](planning/mid-term/README.md#measured-optimization) | +| GPU memory budgets/eviction | Deferred · phase 3 | Resource handles and caches are implemented, but automatic residency budgets, eviction and GPU-memory policy are absent. | [plan](planning/long-term/README.md#residency-accounting) | +| Independent provider cache domains | Deferred · phase 3 | Global asset managers currently use one active provider/loading-owner domain; parallel independent cache domains are unsupported. | [plan](planning/long-term/README.md#residency-accounting) | +| Monitor hotplug event service | Deferred · phase 3 | Monitor enumeration exists; an engine-level dynamic monitor-added/removed subscription and lifecycle contract is absent. | [plan](planning/long-term/README.md#monitor-hotplug) | | Windows worker CPU affinity | Deferred · phase 4 | Provide calling-thread CPU binding on Windows while preserving memory placement and inherited CPU eligibility. Preferred policies currently fall back; required CPU/cache locality is unavailable. | [Windows affinity scope](planning/long-term/README.md#windows-worker-affinity) | | macOS native acceptance | Deferred · phase 4 | Validate dependency configuration, windowing, input and graphics behavior on macOS. | [platform-acceptance.md](planning/long-term/platform-acceptance.md) | | Wayland native acceptance | Deferred · phase 4 | Verify GLFW Wayland windows, focus, input and OpenGL presentation; X11 is the active desktop acceptance path. | [platform-acceptance.md](planning/long-term/platform-acceptance.md) | -| Steam API services | Deferred · phase 4 | Define application-scoped service session, callback scheduling, SDK selection and integration requirements from a real consumer. | [README.md](planning/long-term/README.md) | +| Steam API services | Deferred · phase 4 | Define application-scoped service session, callback scheduling, SDK selection and integration requirements from a real consumer. | [plan](planning/long-term/README.md#steam-api-and-input-integration) | | Spatial/positional audio | Deferred · phase 4 | Audio handles expose gain, looping and playback, but no listener/source coordinates or spatial mixing contract. | [system.h](../projects/engine/include/cheryl/audio/system.h) | | OS clipboard services | Deferred · phase 4 | Both toolkit adapters reject clipboard API requests rather than reading or writing OS clipboard data. | [session.cpp](../projects/modules/ui/tgui/src/session.cpp) | | IME composition/preedit | Deferred · phase 4 | Committed Unicode text is delivered, but multi-stage input-method composition/preedit integration is missing. | [session.cpp](../projects/modules/ui/rmlui/src/session.cpp) | @@ -120,7 +126,7 @@ within each phase. | SimulationScheduler | Implemented | Variable/fixed cadence with bounded recovery, skipped time and catch-up policy. | [simulation-scheduler.h](../projects/engine/include/cheryl/core/game-framework/simulation-scheduler.h) | | TickContext | Implemented | Provides simulation delta, aggregated input, framebuffer/logical dimensions and stop-request access. | [tick-context.h](../projects/engine/include/cheryl/core/game-framework/tick-context.h) | | `CE::Workers::Pool` | Implemented | Shared threads with futures, acceptance/closure, draining and exception delivery. | [worker-pool.h](../projects/engine/include/cheryl/core/engine/worker-pool.h) | -| Worker CPU placement | Implemented | Application-owned topology providers, optional runtime hwloc core/cache discovery and calling-thread binding where supported, automatic/reuse/spread policies and validated C++/JSON settings. Windows binding remains deferred to phase 4; NUMA memory placement remains deferred. Topology is not a speedup guarantee. | [worker contract](runtime/worker-execution.md) | +| Worker CPU placement | Implemented | Application-owned topology providers, optional runtime hwloc core/cache discovery and calling-thread binding where supported, automatic/reuse/spread policies and validated C++/JSON settings. Windows binding remains deferred to phase 4; NUMA memory placement remains deferred. Topology is not a speedup guarantee. | [worker contract](site/runtime/worker-execution.md) | | Weighted worker groups | Implemented | Isolated logical workloads with concurrency limits, priority, fair scheduling and statistics. | [worker-pool.cpp](../projects/engine/src/core/engine/worker-pool.cpp) | | PlatformDispatcher | Implemented | Submits captured work for execution on the native graphics/platform owner. | [platform-dispatcher.h](../projects/engine/include/cheryl/core/engine/platform-dispatcher.h) | | SimulationDispatcher | Implemented | Delivers queued callbacks on the simulation owner through future-bearing submissions. | [simulation-dispatcher.h](../projects/engine/include/cheryl/core/engine/simulation-dispatcher.h) | @@ -141,13 +147,13 @@ within each phase. | Capture leases | Implemented | RAII capture subscriptions for ordered events and committed text. | [input-capture.h](../projects/engine/include/cheryl/core/controls/input-capture.h) | | Keyboard focus routing | Implemented | Focus IDs, exclusive routing and epoch-aware leases for UI/gameplay consumers. | [input-routing.h](../projects/engine/include/cheryl/core/controls/input-routing.h) | | GLFW keyboard/mouse adapter | Implemented | Collects GLFW events and maps Gainput/native observations to Cheryl records. | [input-system.cpp](../projects/modules/platform/native-glfw/src/core/controls/input-system.cpp) | -| Native controller input (non-HID) | Implemented | Gainput-driven gamepad collection, retained-state and disconnect reconciliation exist; the Linux joystick path has an accepted HID-disabled baseline. | [native module](../projects/modules/platform/native-glfw/README.md#controller-backend-limits) | +| Native controller input (non-HID) | Implemented | Gainput-driven gamepad collection, retained-state and disconnect reconciliation exist; the Linux joystick path has an accepted HID-disabled baseline. | [native module](site/modules/native-glfw.md#controller-backend-limits) | | Display/monitor abstraction | Implemented | Monitor enumeration, primary selection, logical and framebuffer sizes and content scale. | [display-system-interface.h](../projects/engine/include/cheryl/core/display/display-system-interface.h) | | GLFW window modes | Implemented | Native resizing, normal/borderless/fullscreen transitions, close detection and cursor visibility. | [window.cpp](../projects/modules/platform/native-glfw/src/core/display/window.cpp) | | **Rendering** | | | | -| Positioned text runs | Implemented | Opt-in prepared glyph placement geometry and contiguous atlas-page draws preserve authored order, clipping, sampling and retained generations; arbitrary shader and exact boundary-pixel semantics need the per-glyph path. | [text contract](assets/text-layout.md#positioned-text-runs) | -| TGUI texture sampling | Implemented | Texture/font smoothing is copied into retained draws; immutable nearest/linear samplers share image generations without mutation. Toolkit policies disable mipmaps/anisotropy while ordinary engine defaults request anisotropy. Selected Linux sampling/UI checks are accepted; native nearest-font appearance remains separate. | [TGUI contract](../projects/modules/ui/tgui/README.md#recording-and-publication), [sampling](rendering/pipelines-and-materials.md#immutable-sampling) | -| Shader manifest schema and indexed material loading | Implemented · demo QA pending | Graphics 2.0 indexes select CPU-only program/material recipes, owned source bytes and backend-owned construction. Main text/image recipes migrated; textures and shaders stay independent, with optional asset selections and explicit incremental replacement. Linux CPU/backend checks are accepted; shader-pair failure/recovery QA remains pending. | [manifest guide](assets/asset-manifests.md#shadermaterial-definitions), [loading](assets/asset-loading.md) | +| Positioned text runs | Implemented | Opt-in prepared glyph placement geometry and contiguous atlas-page draws preserve authored order, clipping, sampling and retained generations; arbitrary shader and exact boundary-pixel semantics need the per-glyph path. | [text contract](site/assets/text-layout.md#positioned-text-runs) | +| TGUI texture sampling | Implemented | Texture/font smoothing is copied into retained draws; immutable nearest/linear samplers share image generations without mutation. Toolkit policies disable mipmaps/anisotropy while ordinary engine defaults request anisotropy. Selected Linux sampling/UI checks are accepted; native nearest-font appearance remains separate. | [TGUI contract](site/modules/tgui.md#recording-and-publication), [sampling](site/rendering/pipelines-and-materials.md#immutable-sampling) | +| Shader manifest schema and indexed material loading | Implemented · demo QA pending | Graphics 2.0 indexes select CPU-only program/material recipes, owned source bytes and backend-owned construction. Main text/image recipes migrated; textures and shaders stay independent, with optional asset selections and explicit incremental replacement. Linux CPU/backend checks are accepted; shader-pair failure/recovery QA remains pending. | [manifest guide](site/assets/asset-manifests.md#shadermaterial-definitions), [loading](site/assets/asset-loading.md) | | Renderer abstraction | Implemented | Backend-neutral lifecycle, frame rendering, viewport, clear and camera matrix operations. | [renderer.h](../projects/engine/include/cheryl/core/rendering/renderer.h) | | OpenGL backend | Implemented | GL-based pass playback, context validation, native rendering and resource maintenance. | [renderer.cpp](../projects/modules/graphics/opengl/src/backends/opengl/renderer.cpp) | | GLFW OpenGL context | Implemented | Context creation, activation and proc loading through the GLFW graphics module. | [glfw-context.h](../projects/modules/graphics/opengl/include/cheryl/backends/opengl/glfw-context.h) | @@ -203,19 +209,19 @@ within each phase. | Pivot/anchor geometry | Implemented | Pivot presets, anchor conversion, quad/strip generation and Y-up geometry. | [anchor.h](../projects/engine/include/cheryl/math/anchor.h) | | General math utilities | Implemented | Byte, pointer, binary, fitting, numeric and time helper functions. | [math/](../projects/engine/include/cheryl/math/) | | File manager and path I/O | Implemented | Core file-manager interface and path-oriented file operations, distinct from installed font discovery. | [file-mgr.h](../projects/engine/include/cheryl/core/resources/fileio/file-mgr.h) | -| External Debug terminal | Implemented · Linux desktop acceptance pending | Optional Linux module owns process capture/viewer and test-report routing. Selection defaults on for supported single-configuration Debug builds; explicit ON includes every configuration, with non-Debug startup requiring a runtime enable option. Engine supplies an optional startup callback attachment. | [module guide](../projects/modules/platform/debug-terminal-linux/README.md), [desktop QA](testing-requests.md#tr18-qa-linux-debug-terminal-on-a-real-desktop) | +| External Debug terminal | Implemented · Linux desktop acceptance pending | Optional Linux module owns process capture/viewer and test-report routing. Selection defaults on for supported single-configuration Debug builds; explicit ON includes every configuration, with non-Debug startup requiring a runtime enable option. Engine supplies an optional startup callback attachment. | [module guide](site/modules/debug-terminal-linux.md), [desktop QA](testing-requests.md#tr18-qa-linux-debug-terminal-on-a-real-desktop) | | **Build and verification** | | | | -| Installed CMake package | Implemented | CherylSDK component exports prebuilt selected libraries, headers, dependency targets and licenses; selected hosted Linux and Windows installed consumers are accepted. Windows bundles native dependencies. | [SDK consumption](development/consuming-engine.md#prebuilt-development-sdks) | -| GitHub CI and SDK artifacts | Implemented | Linux GCC/Clang and Windows MSVC focused builds/tests plus relocated SDK consumers; successful main validation collects compiled development SDK artifacts without applications or release publication. Hosted builds and relocated consumers are accepted with explicit Windows skipped-case limits; first main-push artifact collection remains pending. | [CI matrix](development/ci.md), [TR20](testing-requests.md#tr20-automated-main-push-sdk-collection) | +| Installed CMake package | Implemented | CherylSDK component exports prebuilt selected libraries, headers, dependency targets and licenses; selected hosted Linux and Windows installed consumers are accepted. Windows bundles native dependencies. | [SDK consumption](site/development/consuming-engine.md#prebuilt-development-sdks) | +| GitHub CI and SDK artifacts | Implemented | Linux GCC/Clang and Windows MSVC focused builds/tests plus relocated SDK consumers; successful main validation collects compiled development SDK artifacts without applications or release publication. Hosted builds and relocated consumers are accepted with explicit Windows skipped-case limits; first main-push artifact collection remains pending. | [CI matrix](site/development/ci.md), [TR20](testing-requests.md#tr20-automated-main-push-sdk-collection) | | Modular CMake root | Implemented | C++23 build splits Engine and native graphics/platform/UI/audio owners. | [CMakeLists.txt](../CMakeLists.txt) | | CMake option selection | Implemented | Root switches for GLFW, OpenGL, TGUI, RmlUi, miniaudio, native input, demo and test modes. | [CherylOptions.cmake](../cmake/CherylOptions.cmake) | | Named library target aliases | Implemented | Consistent identities such as Cheryl::Engine, Cheryl::NativeGLFW and Cheryl::Audio::Miniaudio. | [CherylTargets.cmake](../cmake/CherylTargets.cmake) | | Standalone module composition | Implemented | Module entry points assemble their local dependency graph without requiring every other module. | [CherylModules.cmake](../cmake/CherylModules.cmake) | -| Build-tree consumers | Implemented | Embeddable source consumers can use add_subdirectory() and target-linked Engine/module libraries. | [consuming-engine.md](development/consuming-engine.md) | +| Build-tree consumers | Implemented | Embeddable source consumers can use add_subdirectory() and target-linked Engine/module libraries. | [consuming-engine.md](site/development/consuming-engine.md) | | GoogleTest suites | Implemented | Engine/module unit, suite, aggregate and optional acceptance runners are declared and have test source; subject guides own configuration-specific acceptance. | [CherylTests.cmake](../cmake/CherylTests.cmake) | | CTest discovery | Implemented | Selected GoogleTest runners are registered with CTest using test discovery. | [CherylTests.cmake](../cmake/CherylTests.cmake) | | OpenGL native acceptance sources | Implemented | Existing tests exercise clipping, resource lifetime, input recovery, renderer shutdown and failure paths; source presence does not establish every native path. | [native-opengl.cpp](../projects/modules/graphics/opengl/tests/acceptance/src/native-opengl.cpp) | -| Cross-UI integration test | Implemented | Source/target for a consumer linking both TGUI and RmlUi modules; the UI guide records selected root-composition acceptance. | [UI validation](development/ui-adapters.md#repeating-linux-root-validation) | +| Cross-UI integration test | Implemented | Source/target for a consumer linking both TGUI and RmlUi modules; the UI guide records selected root-composition acceptance. | [UI validation](site/development/ui-adapters.md#repeating-linux-root-validation) | | Consumer header probes | Implemented | Checks selected public API headers and detects accidental native GL/GLFW leakage into neutral includes. | [CherylConsumers.cmake](../cmake/CherylConsumers.cmake) | | Native demo application | Implemented | Runnable composition example for input, camera, asset drawing, text and optional UI toolkits. | [main.cpp](../projects/apps/demo/src/main.cpp) | | Debug sanitizers | Implemented | Non-MSVC Debug configuration enables address/undefined-behavior sanitizers. | [CMakeLists.txt](../CMakeLists.txt) | @@ -225,35 +231,72 @@ within each phase. ## Assessment and direction -The review's qualitative estimate of foundational architecture maturity is -approximately **85%**. It concerns established engine foundations, not completion -of the roadmap, and is not calculated from feature counts, coverage or test results. +The original review's qualitative estimate of foundational architecture maturity +was approximately **85%**. It concerned established engine foundations and was not +calculated from feature counts, coverage or test results. The implemented facilities support a desktop 2D consumer; higher-level game services remain consumer-selected. Cache/core locality, positioned text submission and CI/SDK source work are implemented. Selected Linux controlled/native worker, text-run, sampling/UI and relocated SDK checks are accepted within their -[subject-guide limits](README.md). Exact hosted Linux/Windows execution and the main -SDK collection gate remain pending; local Linux results do not establish Windows -runtime support. Terminal configuration/desktop acceptance, independent Engine-only -Release graph verification and targeted shader-reload/artwork QA remain in the -[testing queue](testing-requests.md). NUMA remains deferred indefinitely; general -packet merging and sorting require separate scope and workload evidence. +[subject-guide limits](README.md). Hosted Linux/Windows builds and relocated SDK +consumers, the independent Engine-only Release graph and the terminal configuration +matrix are accepted within their recorded limits. Desktop/device acceptance and +the first main-push SDK collection remain separate below. NUMA remains deferred +indefinitely; general packet merging and sorting require separate scope and workload +evidence. Recorded Linux acceptance covers the initial -[Unicode scope](assets/text-layout.md#native-acceptance-scope), -[audio baseline](../projects/modules/audio/miniaudio/README.md#acceptance-boundaries) +[Unicode scope](site/assets/text-layout.md#native-acceptance-scope), +[audio baseline](site/modules/miniaudio.md#acceptance-boundaries) and [sprite/tile samples](../projects/apps/demo/README.md#tile-and-sprite-samples), as well as the selected startup/UI composition. Their guides retain meaningful coverage limits, including bidi observations, compressed streaming, shader-failure recovery and simultaneous close/resize. Dedicated new regressions and supplied-SDK composition gaps remain in their owning plans; they are not blanket pending QA. -The [assembly guide](architecture/assembling-engine.md) explains system composition; +The [integration guide](site/development/consuming-engine.md#context-assembly) explains system composition; [subject guides](README.md) own detailed contracts. The -[roadmap](planning/develop-review-and-development-plan.md) owns sequencing, +[planning catalogue](planning/README.md) routes to subject plans, [long-term plans](planning/long-term/README.md) retain platform/consumer prerequisites, and the [testing queue](testing-requests.md) owns pending executable acceptance. +## Remaining acceptance + +Linux/GLFW/X11/OpenGL is the active native platform. Deferred platform coverage +does not gate Linux work. These remaining checks have independent prerequisites: + +| Work | Next action and owner | +| --- | --- | +| Main-push SDK collection | Verify the first successful main-push collection gate in [TR20](testing-requests.md#tr20-automated-main-push-sdk-collection). Hosted builds and relocated SDK consumers are accepted; PR/develop/manual runs intentionally skip collection. | +| Hosted Windows skipped cases | Supply a CFF font fixture and a temporary directory on the checkout drive, then accept the affected font-allocation and logging cases. The [CI coverage limits](site/development/ci.md#hosted-windows-coverage-limits) retain the exact scope. | +| Linux Debug terminal | Complete real-desktop [TR18](testing-requests.md#tr18-qa-linux-debug-terminal-on-a-real-desktop) with matching probe binaries, including emulator windows and desktop/IDE launch. | +| Demo material/artwork QA | Complete shader-pair failure/recovery and available artwork observations in [TR19](testing-requests.md#tr19-qa-indexed-demo-materials-and-recovered-graphics). Missing images and showcase fixtures remain blockers. | + +The [short-term plan](planning/short-term/README.md) owns unimplemented +font-selection and startup/UI regressions, supplied RmlUi composition acceptance +and the upstream placeholder follow-up. Existing Linux acceptance does not close +those dedicated gaps. The [demo guide](../projects/apps/demo/README.md#repeating-native-qa) +owns reusable native procedures. + +## Supporting plans + +These plans cover follow-up work, asset content and deferred investigations outside +the feature classification counts above. Each has an owning document or section: + +| Work | Readiness | Plan | +| --- | --- | --- | +| Font and startup/UI regression coverage | Existing baseline accepted; dedicated boundary, result-handoff, callback and placeholder cases remain. | [Font selection](planning/short-term/README.md#font-selection-regressions), [Startup/UI](planning/short-term/README.md#startup-and-ui-follow-up) | +| Supplied RmlUi composition | Needs a corrected dependency and independent consuming host. | [Supplied dependency acceptance](planning/short-term/README.md#supplied-rmlui-acceptance) | +| Upstream placeholder correction | Needs an unmodified reproduction and matching regression before submission. | [Upstream follow-up](planning/short-term/README.md#upstream-placeholder-follow-up) | +| Artwork metadata | Needs source evidence or artwork-owner decisions for remaining manifest semantics. | [Unresolved metadata](planning/long-term/asset-manifest-metadata.md) | +| Additional dungeon manifests | Deferred; acquired packages still need separate image inventories and manifests. | [Dungeon packages](planning/long-term/README.md#additional-dungeon-asset-manifests) | +| Atlas padding/repacking | Deferred; scope not selected. | [Atlas scope](planning/long-term/README.md#atlas-padding-and-repacking) | +| NUMA discovery/memory placement | Indefinitely deferred; needs a consumer and multi-node hardware justification. | [NUMA prerequisites](planning/long-term/README.md#numa-locality) | + +Some deferred feature entries have only a description or consumer-required deferral, +not a concrete development plan. The [planning gaps](planning/README.md#topics-awaiting-plans) +identify that remaining scoping work; they are not ready implementation plans. + ## Status maintenance Update an entry when its implementation or acceptance changes, checking current diff --git a/docs/architecture/assembling-engine.md b/docs/architecture/assembling-engine.md deleted file mode 100644 index deb2216a..00000000 --- a/docs/architecture/assembling-engine.md +++ /dev/null @@ -1,109 +0,0 @@ -# Assembling the Engine - -Choose the public entry point for your application, then construct a compatible -set of implementations. [Engine Systems](../engine-systems/index.md) introduces -their responsibilities; the [module guides](../modules/index.md) describe the -available implementations. - -## Facades and public includes - -| Entry point | Use it to | -| --- | --- | -| `Cheryl::Cheryl` | Link the complete configured assembly through one CMake interface target. It forwards usage requirements to the existing libraries; it creates no combined binary. | -| `` | Include the public headers enabled by your linked targets. This umbrella includes declarations; target selection supplies libraries and feature definitions. | -| `CE::Application::make_startup()` | Use the standard configuration, validation and backend-composition path instead of assembling its callbacks yourself. | - -For the complete selected assembly: - -```cmake -target_link_libraries(game PRIVATE Cheryl::Cheryl) -``` - -Link individual public targets when you need a narrower set. The same umbrella -then exposes that selection. Source assemblies can include the optional Linux -Debug terminal; installed SDK assemblies omit that source-only attachment. -The [source and SDK consumer guide](../development/consuming-engine.md) -provides complete CMake examples and deployment requirements. - -## Factories and context composition - -With Startup's OpenGL integration and native input selected, an application's -`main()` can use the standard factory: - -```cpp -auto startup = CE::Application::make_startup("My game"); -auto result = startup.initialize(argc, argv); -if (!result.should_start()) - return result.exit_code; - -MyGame game(*result.context); -auto runtime = result.make_runtime(game); -runtime.run(); -``` - -Include ``. Here `MyGame` implements -`CE::Game::AbstractGame`; keep `startup`, `result` and `game` alive through the -session. The [Startup guide](../modules/startup.md) covers initial defaults, -application options and supplied services. Help and configuration errors return -before backend construction; other backend failures propagate. - -For a custom assembly, supply `StartupBackend::create` or construct the lower-level -context directly. A factory returns owned components with compatible platform and -graphics domains; initialization and cleanup retain their owner-thread requirements. - -`CE::Runtime::Context` owns the selected display, presentation, renderer and -resource provider, and owns or explicitly borrows input. `GameRuntime` borrows -that context and the game for one session; `run()` initializes and closes the -adapters around the game hooks. Use the [interface catalogue](interfaces-adapters.md) -when supplying a new implementation. - -`make_startup()` selects its compiled default backend; it does not discover -runtime plugins. The optional [Linux Debug terminal](../modules/debug-terminal-linux.md) -attaches at process startup outside this context graph. Its support hook invokes -the application unchanged when no implementation callback is supplied. - -## Backend boundaries - -| Boundary | Contract | -| --- | --- | -| CPU asset data | Vertex layouts, indexed sprite/tileset and shader recipes, grid generation, owned RGBA pixels and shader-byte snapshots have no graphics-context dependency. Loader preparation runs independently of a provider. | -| Display and presentation | `CE::Platform::DisplaySystem` owns a selected window implementation. `CE::Platform::Window` and monitor snapshots carry neutral data; `CE::Rendering::PresentationSurface` presents. GLFW handles remain inside the platform/backend implementation. The renderer does not own the display. | -| Input | `CE::Input::System` supplies State plus independently requested ordered Events/Text. GLFW/Gainput translation stays in its adapter; games consume tick values and routed views. The default factory owns input; an explicit-reference overload borrows it. | -| Draw and material | Simulation writes complete ordered `CE::Rendering::Frame` passes. CPU submission helpers resolve sprite, tile, graphic, and glyph packets retaining geometry/material generations and copied values. `CE::Rendering::OpenGL::PipelineBindings` maps contract keys to native names; each pipeline applies full fixed state. | -| Resource creation and caches | `CE::Rendering::Resources::Provider` uploads decoded pixels and transient vertex spans, creates font atlases, and links programs. Optional sampler and shader-recipe builders reject unsupported requests. Cache readers retain handles under shared locks; construction and retired-handle destruction stay outside locks. One provider/loading thread binds the singleton caches until teardown. | -| Composition and scheduling | One runtime session owns platform polling and presentation. Concurrent mode adds one simulation worker and three reusable frame slots. `platform_dispatcher().submit()` transfers owned resource requests with future results; shutdown cancels pending requests before game cleanup. | -| OpenGL lifetime | Active resource use requires the owner thread and its actual current context. Handles retire without OpenGL calls from their destructors. Renderer shutdown restores its context, deletes tracked handles, closes their lifetime, and releases the context. Failed destructor cleanup invalidates retained handles. | -| Portable integration | The in-memory display/window, input, presentation, renderer, and resource provider exercise both runtime modes without OpenGL/GLFW headers. Aggregate regressions cover lifecycle, dispatch, cache publication, materials, preparation, and input routing. | - -## Using the assembled context - -Use `engine.display()`, `engine.window()`, and `engine.surface()` for the selected -platform and presentation contracts. -Use `engine.resources()` for backend asset creation after renderer initialization. -Construct an owned `CE::Assets::Loading::Loader(root)` for each root. `manifests()` returns a retained -immutable snapshot; singleton compatibility rejects a different root after its -first initialization. - -The [loading guide](../assets/asset-loading.md) owns preparation/publication and -application bootstrap; the [render guide](../rendering/pipelines-and-materials.md) -owns program/material construction and packet resolution. Optional -[audio](../runtime/audio.md) is application-owned outside the runtime adapter graph. - -## Execution and shutdown - -Games request shutdown from `update()` with `tick.request_stop()`. The tick carries -the runtime's shared stop state, so games need no runtime singleton or callback -wiring. Applications holding the runtime can still call `CE::Runtime::GameRuntime::stop()`; -both requests use the same normal cleanup sequence. The stop request and tick -lifetime contracts are in [runtime-frame-boundary.md](../runtime/runtime-frame-boundary.md). - -[Threads and Data Flow](threads-data-flow.md) explains owner dispatch, context-tracked -worker groups and the separate simulation thread. [Ownership and Cleanup](ownership-cleanup.md) -connects producer quiescence, accepted-work completion and game/adapter release; -the detailed runtime lifecycle owns their exact ordering and failure behavior. - -## Current limits - -The [status inventory](../STATUS.md) records source-backed capabilities and known -gaps at its review baseline. The [planning catalogue](../planning/README.md) links -their owning plans; each subject guide defines its current capability limits. diff --git a/docs/architecture/index.md b/docs/architecture/index.md deleted file mode 100644 index e680735a..00000000 --- a/docs/architecture/index.md +++ /dev/null @@ -1,16 +0,0 @@ -# Architecture - -Connect engine systems while preserving their ownership and execution rules. -Start with the interfaces available to implement, assemble compatible components, -decide who keeps them alive, then coordinate their work. - -| Need | Start here | -| --- | --- | -| Implement or replace an integration | [Interfaces and Adapters](interfaces-adapters.md) | -| Select public entry points and construct a compatible context | [Assembling the Engine](assembling-engine.md) | -| Keep borrowed targets and retained resources alive through cleanup | [Ownership and Cleanup](ownership-cleanup.md) | -| Choose an execution owner and transfer work or results | [Threads and Data Flow](threads-data-flow.md) | - -[Engine Systems](../engine-systems/index.md) introduces each system and its public -types. [Detailed Guides](../guides/index.md) supplies the operating procedures and -exact contracts for a particular task. diff --git a/docs/development/modules.md b/docs/development/modules.md deleted file mode 100644 index 48ad2d09..00000000 --- a/docs/development/modules.md +++ /dev/null @@ -1,250 +0,0 @@ -# Engine and integration modules - -Cheryl uses one shared `Cheryl::Engine` and independently selected integration -libraries. Each owner declares its sources, public includes, dependencies and tests. -The root selects the assembly. The [module index](../../projects/modules/README.md) -maps owners to Engine contracts and links their APIs; [build options](building.md#configuration-options) -control selection. - -Engine has no reverse dependency on an integration. Logging, memory, workers, -events, assets and utilities remain within the one Engine library. Platform owners -implement display/window/input contracts, graphics owners implement rendering and -resources, UI owners consume neutral contracts, and audio owners implement playback. -The optional Linux Debug terminal owns process output/viewer behavior outside the -runtime adapter graph. Its Engine support attachment accepts a module callback; -an absent implementation leaves application startup unchanged. - -The public [Startup module](../modules/startup.md), `Cheryl::Startup`, supplies -standard application bootstrap and privately integrates configured backends. -`Cheryl::OpenGL::Startup` is a compatibility interface over Startup/OpenGL rather -than another runtime library. CLI11 remains the explicit public option frontend; -Engine and OpenGL do not depend back on Startup. - -The optional [Workers module](../modules/workers.md), `Cheryl::Workers`, groups -configuration loading and topology integration with independent JSON/hwloc -features. Core pool execution/scheduling remains in Engine. Its capability-specific -`Cheryl::WorkerTopology::Hwloc` and `Cheryl::WorkerTopology::Json` compatibility -names refer to the same selected implementation. Private support belongs inside -its owning binary; configurable build policy alone is not a public module. - -```mermaid -flowchart LR - Application --> Engine - Application --> Startup - Startup --> Engine - Startup --> OpenGL - Application --> NativeGLFW - Application --> OpenGL - OpenGL --> NativeGLFW - OpenGL --> Engine - NativeGLFW --> Engine - Application --> UI[Selected UI adapter] - UI --> Engine - Application --> Audio[Selected audio backend] - Audio --> Engine -``` - -## Adapter contracts - -Use [Interfaces and Adapters](../architecture/interfaces-adapters.md) to find the -required runtime interfaces, optional capabilities, existing implementations and -replacement dependencies. [Assembling the Engine](../architecture/assembling-engine.md) -explains how factories connect them. This guide covers creating and composing the -module that owns an implementation. - -### SDL3 scaffold - -`CHERYL_BUILD_NATIVE_SDL3=ON` adds the source-only -`Cheryl::NativeSDL3::Scaffold` interface target from -[`platform/native-sdl3`](../../projects/modules/platform/native-sdl3). -The option defaults to `OFF`. Its -[`cheryl/backends/native-sdl3.h`](../../projects/modules/platform/native-sdl3/include/cheryl/backends/native-sdl3.h) -includes the existing Engine contracts and forward-declares -`CE::Platform::SDL3::Window`, `DisplaySystem` and `InputSystem`. - -The scaffold has no runtime implementations, SDL3 dependency discovery or startup -factory. It is not registered as an SDK component or a selectable application -backend. Native GLFW remains the implemented platform integration. SDL3 runtime -implementation, OpenGL/startup selection and GLFW deprecation policy remain -future work. - -## Module boundary criteria - -A separate module is justified when it gives an application a concrete choice or -isolation benefit: omitting a dependency, selecting an alternative implementation, -or testing an external integration independently. A directory, internal interface -or small caller count does not by itself justify a library boundary. Cheryl keeps -one shared `Cheryl::Engine`. - -Keep the selected Native GLFW, whole OpenGL and independent TGUI/RmlUi owners -cohesive unless a consumer demonstrates a benefit from another boundary. Native -input depends on a live window; dividing display and input splits that coupled -lifetime. OpenGL's context interface permits another window integration within its -owner and does not itself justify context/bridge libraries. - -Before introducing another library target, record: - -1. the concrete omit/replace/test benefit; -2. the engine contract the owner implements; -3. dependencies that become optional or isolated; -4. lifetime/dependency direction and how cycles are avoided; -5. test ownership and the smallest independent consumer proving the boundary; -6. compatibility consequences for current consumers. - -If those points do not identify a real benefit, keep the code inside the existing -owner. Logging, memory, workers, events, general utilities and similar internal -facilities remain engine organization unless a consumer demonstrates otherwise. -Evaluate architectural interfaces by their contract rather than deleting them -because they have few callers. - -## Select an assembly - -The root defaults to Startup, Workers, Native GLFW, OpenGL, TGUI, RmlUi and miniaudio; the demo is -opt-in. UI and audio selection is independent of platform/graphics selection. -OpenGL requires Native GLFW; the demo also requires native input. For Engine alone, -disable every module, including Startup/Workers, as shown in the -[engine-only configuration](building.md#engine-only-configuration). -On Linux GNU/Clang this includes `CHERYL_BUILD_DEBUG_TERMINAL_LINUX=OFF`. -The root automatically links the selected terminal interface to its demo/shared -test entry point and the source `Cheryl::Cheryl` facade. External applications can -also select it individually through `Cheryl::DebugTerminal::Linux`. Its configuration -policy and standalone probes are in the -[owner guide](../../projects/modules/platform/debug-terminal-linux/README.md). - -Applications link public targets, for example: - -```cmake -target_link_libraries(game PRIVATE Cheryl::Engine Cheryl::NativeGLFW Cheryl::OpenGL) -``` - -Alternatively, link `Cheryl::Cheryl` to select the complete configured assembly, -including its selected public capabilities: - -```cmake -target_link_libraries(game PRIVATE Cheryl::Cheryl) -``` - -This facade is an interface target over the existing libraries. Its source assembly -includes configured diagnostic support; the installed SDK omits the source-only -Linux Debug terminal and its support attachment. - -Both source and installed SDK consumers can then include `` for -the public API of their linked assembly. `` stays neutral; -`` and `` expose the linked module and support -groups. Per-owner umbrellas live under `cheryl/modules/` and `cheryl/support/`, so -these APIs appear under the same `cheryl/` include prefix in IDE completion. -The [consumer guide](consuming-engine.md#dependencies-and-headers) describes the -include layout and source-versus-SDK workflows. - -Linking Engine alone supplies neutral contracts. Targets propagate public include -roots, C++23, logging policy, module feature definitions and dependency requirements; -raw archive consumers must supply their complete link closure. Headers alone do -not select a backend or add its libraries. Concrete classes remain available through -granular owner headers, such as `cheryl/backends/native-glfw.h`. - -`CHERYL_NATIVE_INPUT=OFF` omits Gainput and the default-input factory overload. -`CHERYL_NATIVE_NULL_PLATFORM=ON` configures an owned GLFW without X11/Wayland; -a supplied GLFW target keeps its host settings. The [build guide](building.md) -owns their configuration and dependency details. - -Bundled compiled dependencies have one Cheryl binary owner: Engine owns logging, -font/text and image implementations; Native GLFW owns GLFW/Gainput/HID; OpenGL -owns GLAD; each UI module owns its toolkit implementation and exports its native -authoring API. Startup owns selected integration glue and Workers owns optional -topology/configuration adaptation. Header/template usage such as GLM, CTTI and -CLI11 remains explicitly public. See [dependency contracts](consuming-engine.md#dependencies-and-headers). - -Selection preserves owner lifetimes: input detaches while its window is live, -resources retire before context destruction, and runtime shutdown settles workers -and recycles frames before game/adapter cleanup. - -## Standalone modules - -Standalone entry points locate the enclosing checkout's shared helpers and Engine -automatically. Set an absolute `CHERYL_REPOSITORY_ROOT` when selecting another -checkout. Each owner reuses a supplied `Cheryl::Engine` or adds Engine from that -root. OpenGL also reuses `Cheryl::NativeGLFW` or requires -`CHERYL_NATIVE_GLFW_SOURCE`. Bootstrapping suppresses other owners, demo and Engine -tests in a local variable scope, without rewriting a host's cache. - -This builds only the OpenGL owner and its dependencies: - -```sh -( - set -e - cd "$(git rev-parse --show-toplevel)" - cmake -S projects/modules/graphics/opengl -B build/opengl-module -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCHERYL_REPOSITORY_ROOT="$PWD" \ - -DCHERYL_NATIVE_GLFW_SOURCE="$PWD/projects/modules/platform/native-glfw" - cmake --build build/opengl-module --target module_opengl --parallel -) -``` - -Other module directories are standalone source entry points too. Module-local -checks use `CHERYL_BUILD_TESTS=ON` without enabling Engine's broader suites. -Hosts own their root `enable_testing()` policy. Independent consumers use the -owner's `tests/consumer/` entry point. Prebuilt selected owners are also available -through the [installed SDK](consuming-engine.md#prebuilt-development-sdks), which -uses the same public aliases and its producer's compiled configuration. - -Adding a source module beside a supplied SDK leaves the SDK's `Cheryl::Cheryl` -selection unchanged. Link that source module's target explicitly alongside the -SDK facade; its public feature definition enables its umbrella in the same include. - -## Add a module - -Resolve the boundary criteria above before adding a target. Then: - -1. Create an owner under `projects/modules///` with `include/`, `src/` - and `tests/`. Keep public contracts under `include/cheryl/`; implementation - helpers and SDK details stay private where the API permits it. Add one - owner-local `include/cheryl/modules/.h` umbrella that includes its public - contracts and excludes `internals/`. Support libraries use the corresponding - `include/cheryl/support/` location. Include the owner umbrella from its selected - group in `cheryl/modules.h` or `cheryl/support.h`. -2. Copy [Module.cmake.in](../../cmake/templates/Module.cmake.in) into the owner's - `CMakeLists.txt`. Replace `MODULE_NAME`, the uppercase `MODULE_ID` and public - `MODULE_ALIAS`. `cheryl_prepare_module()` reuses or bootstraps Engine; pass - `REQUIRES_NATIVE` only when Native GLFW is a real prerequisite. -3. Declare shared metadata using the same ID in the files below. Add SDK discovery - inside the selected owner, reusing compatible supplied targets before owned - sources. Publish required header dependencies with `PUBLIC`; keep private - implementation dependencies `PRIVATE`. Call - `cheryl_register_public_component(target FEATURE CHERYL_HAS_)` from - [CherylPublicHeaders.cmake](../../cmake/CherylPublicHeaders.cmake) to register - the owner's public feature definition and add its target to `Cheryl::Cheryl`. - Use that same feature definition to guard its group umbrella include. Do not - rewrite supplied targets/cache. -4. Declare a root selection with `cheryl_option(name description default_value)` - in [CherylOptions.cmake](../../cmake/CherylOptions.cmake), and conditionally add - the owner in the root [CMakeLists.txt](../../CMakeLists.txt). No other module - should select this owner implicitly. -5. Add owner-local behavior suites and an independent consumer with first-include - probes. Keep cross-owner cases in `projects/tests/`. Document the public alias, - dependency selection, lifetime/thread requirements and capability limits in the - owner's README, and add it to the module index. - -| Metadata file | Module declaration | -| --- | --- | -| [CherylVersions.cmake](../../cmake/CherylVersions.cmake) | `VERSION_PROJECT_MODULE_` | -| [CherylTargets.cmake](../../cmake/CherylTargets.cmake) | `TARGET_LIB_MODULE_` and test target identities | -| [CherylOutputs.cmake](../../cmake/CherylOutputs.cmake) | `OUTPUT_LIB_MODULE_` and runner artifact names | -| [CherylLinkage.cmake](../../cmake/CherylLinkage.cmake) | Scoped `LINKAGE_PUBLIC_*` / `LINKAGE_PRIVATE_*` requirements | - -Declaration files reload metadata into the caller's scope; helper definitions are -guarded. Load helpers before declaring a standalone project so versions and target -names are available. The template collects only its owner's `src/*.cpp` with -`CONFIGURE_DEPENDS`, making source additions/removals trigger regeneration. Keep -conditional implementations, generated sources and tests in their own selections. - -[CherylTests.cmake](../../cmake/CherylTests.cmake) creates an owner aggregate with -`cheryl_add_test_aggregate(... OWNER)`; set `CHERYL_TEST_AGGREGATE` before adding -focused suites through `cheryl_add_test_suite`. Suites compile cases once into -object targets reused by focused, owner and assembly runners. Keep test hooks on -case targets so applications do not inherit them. [Test selection](testing.md) -covers runner options; [architecture validation](architecture-validation.md) -covers isolated graphs, supplied-target reuse and native acceptance. - -UI-specific font, alpha, input and publication requirements are in -[Writing a UI adapter](ui-adapters.md). diff --git a/docs/external-work/rmlui-placeholder-issue.md b/docs/external-work/rmlui-placeholder-issue.md deleted file mode 100644 index 2816b1b0..00000000 --- a/docs/external-work/rmlui-placeholder-issue.md +++ /dev/null @@ -1,249 +0,0 @@ -# RmlUi placeholder hit-testing: issue draft and investigation guide - -This is a private, AI-authored study document, checked against the source and RmlUi's AI policy on 2026-10-08. The issue-shaped section below preserves the report and diagnosis; it is **not a body to paste into RmlUi's tracker**. Their [AI policy](https://github.com/mikke89/RmlUi/blob/master/AI_POLICY.md) requires human-authored GitHub communication. The guide explains how to investigate this yourself and write your own submission. - -No upstream issue or PR has been opened. No build, test, debugger session, or independent crash reproduction was run for this analysis. The proposed fix and testing instructions have not been validated by execution. - -## Issue-style study draft - -### Title: Text input placeholder hit-testing uses display lengths with an empty editable value - -Clicking an empty text input while its placeholder is visible crashes my application's demo. This started after replacing an editable initial `value` with the native `placeholder` attribute: - -```xml - -``` - -The dependency is RmlUi 6.3, pinned to [`ba95ffe8bfb6370efb2cdcca927eaad4710c5413`](https://github.com/mikke89/RmlUi/commit/ba95ffe8bfb6370efb2cdcca927eaad4710c5413). The application loads its font, shows the document, and updates the context before forwarding input. The placeholder is displayed as expected. The application does not implement a custom text-input widget or manually set `placeholder-shown`. - -Expected behavior: clicking focuses the field, the editable value remains empty, and typing replaces the help text with the entered value. - -Observed behavior: the application crashes when the field is clicked with the placeholder showing. There is no captured backtrace or sanitizer report yet, and this has not been reproduced in a standalone upstream sample. The following diagnosis comes from source inspection; attribution of the reported crash to this path still needs runtime confirmation. - -### Suspected failure path - -The widget distinguishes the displayed text from the editable value, but its character hit-test mixes those two representations. - -1. [`InputTypeText::OnAttributeChange`](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/Elements/InputTypeText.cpp#L41-L64) forwards changes to the native widget. [`SetValueOrPlaceholder`](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/Elements/WidgetTextInput.cpp#L1017-L1030) sets `placeholder-shown` and stores the placeholder in `text_element` when the value is empty. -2. [`WidgetTextInput::GetValue`](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/Elements/WidgetTextInput.cpp#L1008-L1015) returns a static empty string while `placeholder-shown` is set. This keeps the hint out of the editable value. -3. [`FormatText`](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/Elements/WidgetTextInput.cpp#L1285-L1296) generates line content from `text_element` and sets `line.editable_length` from that displayed content. It [retains those lines](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/Elements/WidgetTextInput.cpp#L1403-L1409) and clamps the absolute cursor index, without converting their lengths to the empty editable value. -4. [The mousedown/drag handler](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/Elements/WidgetTextInput.cpp#L693-L723) calls `CalculateCharacterIndex` before clamping the resulting cursor index. -5. [`CalculateCharacterIndex`](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/Elements/WidgetTextInput.cpp#L1120-L1156) takes its starting pointer from `GetValue()` but its offset and length from the displayed line: - -```cpp -const Line& line = lines[line_index]; -const char* p_begin = GetValue().data() + line.value_offset; -const char* p_end = p_begin + line.editable_length; - -for (auto it = StringIteratorU8(p_begin, p_begin, p_end); it;) -{ - ++it; - // Width is then measured from this same range. -} -``` - -For the first line of the original ASCII hint, the mismatch is: - -| Quantity | Value while the placeholder is shown | -| --- | --- | -| Displayed text | `"Click here and type"`, 19 bytes | -| Editable value from `GetValue()` | Empty, 0 bytes | -| First line offset | 0 | -| First line editable length | 19 | -| Supplied hit-test range | Empty value's `data()` through `data() + 19` | -| Valid editable insertion position | 0 | - -That range does not describe the empty string. The [iterator constructor and increment](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/StringUtilities.cpp#L608-L619) trust the supplied range. [Its validity check](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Include/RmlUi/Core/StringUtilities.h#L183-L184) checks pointer boundaries, and [UTF-8 seeking](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Include/RmlUi/Core/StringUtilities.h#L99-L105) does not stop at a NUL terminator. Incrementing can therefore read outside the empty value's valid character range and, depending on storage and how far iteration proceeds, outside its allocation/object. - -This establishes a range mismatch in upstream widget code. It does not establish the exact instruction that crashed the application. Small-string storage, pointer position, and the measured widths can affect whether a particular run crashes or produces a sanitizer diagnostic. - -### Possible fix - -An empty editable value has only one valid insertion position. A candidate fix is to return zero from `CalculateCharacterIndex` before forming pointers or indexing the display lines: - -```cpp -if (GetValue().empty()) - return 0; -``` - -This preserves the distinction between visible hint text and editable content. It also covers the [vertical cursor movement caller](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/Elements/WidgetTextInput.cpp#L892-L924), rather than guarding only a mouse event. This is a proposed correction, not an executed or upstream-accepted fix. - -The exact placement needs review alongside `ideal_cursor_position_to_the_right_of_cursor` and subsequent cursor/selection updates. Placeholder alignment already has [separate cursor handling](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/Elements/WidgetTextInput.cpp#L1451-L1495). A regression should cover those behaviors and the shared textarea widget before claiming the guard is sufficient. - -### Related reports and outstanding evidence - -[Issue #868](https://github.com/mikke89/RmlUi/issues/868) concerns adding placeholder support. [Issue #1008](https://github.com/mikke89/RmlUi/issues/1008) concerns missing-font cursor failures. Both were closed when checked; neither establishes this placeholder/value range mismatch. Searches did not find a matching open issue or PR on 2026-10-08. That is not proof there is none; repeat the search before filing. - -A complete report still needs the exact failing runtime configuration, a small unmodified-upstream reproduction, and a backtrace or sanitizer diagnostic linking the click to this range. It should also say whether the current upstream branch reproduces the problem. A fresh source check of [`master`'s hit-test method](https://github.com/mikke89/RmlUi/blob/master/Source/Core/Elements/WidgetTextInput.cpp#L1120-L1156) on 2026-10-08 found the same pointer/length construction; that is a source observation, not a runtime test. - -## Understanding the issue yourself - -### Trace the two strings before changing anything - -Read the linked methods in this order: `InputTypeText::OnAttributeChange`, `OnPlaceholderAttributeChanged`, `SetValueOrPlaceholder`, `GetValue`, `FormatText`, then the mousedown handler and `CalculateCharacterIndex`. The [attribute callbacks](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/Elements/WidgetTextInput.cpp#L250-L281) connect the markup to the internal state. Finish with `StringIteratorU8::operator++` and `SeekForwardUTF8`, so you can explain the actual read rather than assuming a NUL ends iteration. - -The invariant required by the hit-test is: - -```text -0 <= line.value_offset -0 <= line.editable_length -line.value_offset + line.editable_length <= GetValue().size() -``` - -When the placeholder is showing, line geometry describes the hint while `GetValue().size()` is zero. Displaying a nonempty hint is correct; treating its length as an editable range into the empty string is the problem. - -The original field is a single-line example with offset zero. Wrapped textarea hints can have later lines with nonzero offsets, making the mismatch affect both pointer expressions. Check that case separately rather than assuming a successful input test proves every shared-widget path. - -### Confirm the diagnosis in an isolated upstream checkout - -Use a separate RmlUi checkout and build it as the root project, outside Cheryl's dependency setup. Start with the pinned 6.3 revision, then repeat on the current upstream revision and record its full commit ID. Preserve the shared `extern/rmlui` checkout. - -There is already a Cheryl source-build workaround described below. A demo binary compiled with that workaround cannot demonstrate the unmodified upstream failure. - -For a small upstream reproduction, load a valid font and use a document such as this. It is a suggested fixture, not a reproduction already run: - -```xml - - - Placeholder hit-test - - - - - - -``` - -RmlUi's test shell [loads the bundled LatoLatin fonts](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Samples/shell/src/Shell.cpp#L31-L50). In a standalone sample, explicitly load the font and verify success before using this family. Show the document and call `Context::Update()` before sending input. Confirm that the field has a valid font face, positive content width, and at least one formatted line. This avoids confusing the suspected bug with a missing font or an unformatted field. - -Click at the left, middle, and right of the field. Forward `ProcessMouseMove` before `ProcessMouseButtonDown(0, 0)`, then release with `ProcessMouseButtonUp(0, 0)`. Keep individual trials separate in time unless double-click behavior is the case under investigation. - -With a debugger, break in `CalculateCharacterIndex` and inspect: - -- `parent->IsPseudoClassSet("placeholder-shown")`. -- `GetValue().size()` and `text_element->GetText().size()`. -- `line_index`, `lines.size()`, `line.value_offset`, and `line.editable_length`. -- `p_begin`, `p_end`, and the iterator position as `++it` enters `SeekForwardUTF8`. - -The predicted state is a visible nonempty hint, an empty logical value, and a nonempty range built into that empty value. Capture the stack and these values at the invalid access. Do not manually dereference the bogus range just to print it. - -[The later cursor clamp](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Source/Core/Elements/WidgetTextInput.cpp#L926-L943) can prevent an invalid final insertion index; it cannot undo an earlier invalid read. If your captured failure is instead an empty `lines` access or a missing font, record that difference and revise the diagnosis. - -For AddressSanitizer, also try a much longer ASCII placeholder, for example 256 repeated characters, and click toward the right side of the field. Check the actual formatted line length. A longer range makes crossing the empty string's physical storage more likely; a short hint can read invalid logical content without crossing a sanitizer red zone. No fixed small-string capacity or guaranteed crash should be assumed. - -### Choose and review the C++ change - -Before implementing the guard, explain why zero is the right result for an empty editable value regardless of the hint's width, alignment, or mouse position. The visible hint remains available to layout and rendering. - -Then follow the return value through `SetCursorFromRelativeIndices`, `MoveCursorToCharacterBoundaries`, `UpdateCursorPosition`, and selection updates. For a later textarea display line, a relative zero can still acquire a nonzero absolute offset before clamping. Verify that the final caret and selection remain zero. - -Review whether the guard should precede or follow the reset of `ideal_cursor_position_to_the_right_of_cursor`. Cheryl's provisional guard returns at method entry, skipping that reset. Test transitions from an edited value back to the placeholder and double-click selection instead of assuming that retained state is harmless. - -Simply substituting `text_element->GetText()` for `GetValue()` would make the range describe displayed text, but would also make hit-testing operate on the hint's character positions. Explain how the chosen fix preserves the empty editable value contract. Likewise, changing line lengths during formatting may affect display alignment and other consumers; do not change their meaning without following those uses. - -Keep the upstream correction in C++ and its regression in the upstream test suite. The Cheryl CMake mechanism is downstream packaging, not the underlying fix. - -## Regression and acceptance guidance - -A useful regression exercises the public input path with a real font and completed layout. An assertion that an empty field remains empty is necessary but can pass despite the invalid read; pair behavioral checks with a pre-fix failing memory diagnostic or another demonstrable regression failure. - -The existing [`ElementFormControlInput.cpp` unit-test file](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Tests/Source/UnitTests/ElementFormControlInput.cpp) is a natural place for a focused input regression. Its `form.input.text.no_font` case tests a different condition. The [text-input benchmark](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Tests/Source/Benchmarks/WidgetTextInput.cpp) shows public mouse-event submission, selection inspection, and manual time advancement. - -Write the regression yourself using those fixtures: - -1. Obtain the context from `TestsShell::GetContext()`, load the small document, and find its native `ElementFormControlInput`. Assert successful document/input creation, show the document, and update. Then assert a valid font face, an empty value, and `placeholder-shown` before sending events. -2. Derive pointer coordinates from the field's laid-out content box. Click and release through the context API, update, and inspect `GetValue()` and `GetSelection()`. Both selection endpoints should be zero and selected text empty. -3. Submit text through `ProcessTextInput`. Assert that only the typed text becomes the value and the hint disappears. Clear the value, update, assert the hint returns, and repeat the click. -4. Include a long-placeholder case that demonstrably fails against the unchanged upstream source. Run the same case against the proposed fix. Keep the regression when it passes with the fix. -5. Close the document and use the fixture's shutdown conventions. Advance the test system's manual time between separate clicks, as the benchmark does, to avoid accidentally testing double-clicks. - -Use these cases to assess the shared helper: - -| Case | Required observation | -| --- | --- | -| Original short ASCII hint; left/middle/right clicks | Focus works; value and selection stay empty; no memory diagnostic | -| Long ASCII hint | Demonstrable failure before the change, success after it | -| Drag, Shift-click, and double-click on hint | Hint never becomes selectable or editable content | -| Type, clear, then click again | Typed value is exact; clearing restores safe placeholder behavior | -| Nonempty value and an empty field without a hint | Existing caret placement and selection behavior remain correct | -| Multibyte UTF-8 hint and entered text | Hint stays outside the value; editable text preserves character boundaries | -| Left, center, and right alignment | Caret uses the empty value's aligned insertion position | -| Textarea with wrapped/newline hints; clicks on later lines and vertical movement | No invalid range; final caret/selection stay within the empty value | - -The [test shell](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Tests/Source/Common/TestsShell.cpp#L16-L95) uses its dummy renderer by default while still loading fonts and performing layout. `RMLUI_TESTS_USE_SHELL` enables the native backend window. Start with the default test mode to isolate the widget logic, then perform native mouse QA. A headless pass does not replace checking actual focus, visible caret placement, and placeholder appearance. - -### Suggested Linux sanitizer run - -These commands are for a separate upstream checkout **after you have added the regression**. They are based on RmlUi's [Linux sanitizer workflow](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/.github/workflows/build.yml#L53-L84) and [unit-test target](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/Tests/Source/UnitTests/CMakeLists.txt). They have not been run here. Prerequisites include Clang, CMake with preset support, Ninja, FreeType, SDL2, SDL2_image, and OpenGL development dependencies. - -Replace the absolute checkout path. Name your new cases with the `form.input.text.placeholder` prefix, or adjust the selector. The subshell preserves the caller's working directory. - -```bash -( - set -eu - rmlui_checkout="/absolute/path/to/separate/RmlUi" - cd -- "$rmlui_checkout" - cmake -S . -B Build-placeholder-asan -G Ninja --preset dev \ - -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \ - -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=OFF \ - -DRMLUI_FONT_ENGINE=freetype -DRMLUI_BACKEND=SDL_GL2 \ - -DRMLUI_SDL_VERSION_MAJOR=2 \ - -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \ - -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \ - -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" - cmake --build Build-placeholder-asan --target rmlui_unit_tests --parallel - env -u RMLUI_TESTS_USE_SHELL \ - ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 \ - UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \ - LSAN_OPTIONS="suppressions=$rmlui_checkout/Utilities/lsan.supp:print_suppressions=1" \ - ./Build-placeholder-asan/rmlui_unit_tests \ - --test-case="form.input.text.placeholder*" -) -``` - -First run with the upstream C++ unchanged and the new regression present. Save the failure and its stack. After applying your correction, repeat the same configuration/run; a matching existing build directory can be reused. Check that the selector actually executed your cases: an empty selection is not a passing regression. - -Then run the complete `rmlui_unit_tests` executable with the same sanitizer environment and without the `--test-case` filter. For visual QA, use an upstream sample or the test shell's native mode with a display available. Record which revision, compiler/standard library, font engine, backend, and runtime mode were exercised. - -Use the supported [upstream CI configurations](https://github.com/mikke89/RmlUi/blob/ba95ffe8bfb6370efb2cdcca927eaad4710c5413/.github/workflows/build.yml) to choose additional relevant environment coverage. Different standard libraries can expose this storage bug differently. Distinguish observed results from portability reasoning; unrun CI jobs are not evidence that the change works elsewhere. - -## Cheryl context and the provisional workaround - -The local report followed Cheryl commit `9fa1b75f8ad10602bd8861bbbb1058f4569aec3b`, which replaced an initial value with the native placeholder. The [demo field](../../assets/graphics/ui/demo.rml#L48) and [placeholder style](../../assets/graphics/ui/demo.rml#L25) show the resulting markup; the style only changes its color. - -The [demo setup and update](../../projects/apps/demo/src/rmlui-demo.cpp) load a font, obtain a native `ElementFormControlInput`, show the document, and call `update_time` before handling input. The [session](../../projects/modules/ui/rmlui/src/session.cpp) updates the native context and forwards mouse movement and button events through the normal RmlUi API. Each complete tick batch is now handled once, with application controls before individual records. These observations make a stock-widget failure plausible; they do not rule out every integration fault without a reproduction and stack. - -Cheryl commit `f0a25b2203fcf809601ed8009c8964d8b3bedfef` adds the [source-build workaround](../../projects/modules/ui/rmlui/cmake/RmlUi.cmake). The [module dependency contract](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract) owns generated-source behavior and correction declarations. The guard changes C++ behavior; CMake arranges which source is compiled while leaving the dependency checkout unchanged. - -The helper applies only when Cheryl creates the RmlUi source target. Existing `RmlUi::Core` targets and installed packages bypass it, but the [module](../../projects/modules/ui/rmlui/CMakeLists.txt) rejects them unless their concrete Core target declares `CHERYL_RMLUI_PLACEHOLDER_FIX=TRUE`. The [dependency contract](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract) shows how a provider or consumer can declare a verified correction, including when `RmlUi::Core` is an alias. This declaration must describe a correction already present in the supplied library; it does not install or execute the guard. Neither dependency path has been runtime-validated for this report. Do not describe the local guard as a confirmed fix or use a guarded Cheryl build as the pre-fix upstream baseline. - -## Preparing a submission under RmlUi's AI policy - -The [policy](https://github.com/mikke89/RmlUi/blob/master/AI_POLICY.md) says: - -> All *communication* on the RmlUi GitHub repository must be written by human authors. - -This includes issues, PR descriptions, and comments. Its exceptions are faithful translation and limited, attributed AI quotations with human-written context and predominantly human-authored content. Disclosure does not make an AI-generated issue body acceptable. - -For code, the policy permits smaller AI-assisted PRs subject to disclosure of extent, model, and AI-written parts. The author must understand, review, and test the code, take responsibility for it, and be certain it works in other environments. Large PRs mostly or entirely authored by AI are not accepted. Re-read the live policy before submitting. - -The following is practical preparation, rather than additional rules imposed by that policy: - -1. **Establish firsthand evidence.** Reproduce the failure yourself, capture the relevant state and stack, and determine whether it persists on current upstream. Record actual environment details and results. If attribution remains uncertain, say so. -2. **Explain the defect unaided.** Be able to identify the two strings, explain where the line length originates, show why the iterator's supplied end is wrong, and explain why clamping afterward is insufficient. Also explain why a short hint can appear safe in one environment. -3. **Review the correction yourself.** Understand every changed line and relevant caller, including the state skipped by an early return. Demonstrate pre-fix failure and post-fix success, check the neighboring text-editing behavior, and justify the environment coverage. -4. **Write your own GitHub communication.** Use your observations and understanding to author the issue, PR description, and replies. Do not paste this draft, ask AI to polish it for submission, or treat superficial edits as human authorship. If you quote a small portion, follow the policy's attribution and context requirements. -5. **Disclose AI involvement in your own words if submitting assisted code.** This analysis and the provisional Cheryl guard were AI-authored. If you use the suggested guard, identify that contribution, along with any AI-written tests or later changes. Record the actual model identity from the Codex session/settings rather than guessing; describe what you personally reviewed and tested. -6. **Keep unsupported claims out of the submission.** Include only runtime results you obtained. If you cannot yet meet the PR's understanding, testing, or environment requirements, continue investigating or open a human-authored issue with the limits stated instead of presenting the correction as accepted evidence. - -The useful handoff to maintainers is a small reproducible failure, the violated range invariant, and independently checked behavior after a narrow change. This file preserves the investigation needed to reach that point; that firsthand verification and human-authored submission remain to be done. diff --git a/docs/for-agents/README.md b/docs/for-agents/README.md new file mode 100644 index 00000000..ed8004a5 --- /dev/null +++ b/docs/for-agents/README.md @@ -0,0 +1,50 @@ +# Agent instruction library + +This directory holds focused instructions for working on Cheryl. The root +[AGENTS.md](../../AGENTS.md#instruction-index) is the canonical index: it routes +shared work, investigation, architecture, implementation, testing, Git and +documentation tasks. Start there and read the guides for the current objective. + +The [shared workflow](workflow.md) applies to every task. Focused guides own their +specific methods and rules. Reading a guide does not authorize another work mode +or an operation requiring permission. Existing user authorization persists within +its scope; use it instead of asking again. + +## Documentation and source owners + +[Documentation workflow](documentation.md) owns writing, auditing, usefulness +review and maintenance policy. [API documentation](api-documentation.md) owns +source-comment coverage, compact class declarations and extraction/render checks. +[Documentation generation](documentation-building.md) owns the agent's preparation +and artifact-verification sequence; the linked published tooling guide owns exact +commands, cache behavior and hosting procedures. + +The [documentation directory](../README.md) maps published and repository-only +material. [Published navigation](../site/index.md) owns the manual hierarchy; +subject guides and declaring source own engine contracts. Preserve the agreed +navigation and useful URLs/anchors while improving content and task handoffs. + +These Markdown guides are loaded through the root index. Their directory does +not give them automatic per-directory instruction scope. `docs/AGENTS.md` routes +actual documentation work back to the appropriate guides, including documentation +of the instruction library itself. + +## Maintain this library + +- Keep essential entry safeguards and the task-to-guide index in root `AGENTS.md`. + Put detailed rules and procedures in the focused owner. Summaries link that + owner; they do not introduce competing policy. +- Before moving a rule, account for every requirement, exception, scope boundary + and rationale that still applies. Update references to the old owner and check + that a task entering through the root index reaches the new rule. +- Prefer extending an existing guide. Add a guide when it answers a distinct + recurring task and reduces irrelevant context or repeated searches. Choose a + clear trigger, required inputs, working method, boundaries and completion evidence. +- Record durable user decisions, such as the + [fixes-develop consolidation strategy](git.md#consolidate-branches-into-fixes-develop), + in their subject owner. Keep temporary progress and transcripts out of the library. +- Verify links, headings and policy preservation. Walk representative tasks through + the index: investigation must not silently become implementation; a build must + retain its operation boundary; a merge must reach the recorded history policy. +- Expand from observed needs and recheck the benefit on later tasks. Do not + duplicate the human manual, actual tool configuration or a source inventory. diff --git a/docs/for-agents/api-documentation.md b/docs/for-agents/api-documentation.md new file mode 100644 index 00000000..8d6bdb70 --- /dev/null +++ b/docs/for-agents/api-documentation.md @@ -0,0 +1,254 @@ +# Document the generated API + +Use this guide when creating, repairing or maintaining API source comments and +their generated reference. Read the shared [documentation workflow](documentation.md) +first. Explain the public contract from the responsible declaration, implementation +and relevant tests; inspect what Doxygen, Breathe and Sphinx actually publish. + +## Define the full coverage + +Inventory the current public-header inputs, extracted entities and rendered +articles before a broad pass. The [generator](../site/generation/build.py), +[Doxyfile](../site/generation/Doxyfile.in) and +[Sphinx configuration](../site/generation/conf.py) establish publication scope. +Audit inventories provide the earlier baseline and priorities, not a permanent +list of current declarations. + +Cover every meaningful exposed entity, including members that appear only inside +a type's article: classes, structs, unions, public/protected fields and methods, +constructors, destructors, operators, overloads, enum values, aliases, templates, +concepts, constants, public macros, callbacks and authored header/namespace content. +Check inherited members and specializations. A standalone page count does not +measure this coverage. + +Review each entity's meaning, even when it needs little prose. A transparent alias +or self-contained constant may be complete with a brief explanation or an accurate +declaration and context. A policy field or lifecycle method usually needs much +more. Thorough documentation supplies all applicable meanings; repeating a name +or adding identical boilerplate to every symbol does not satisfy the request. + +Prioritize common lifecycle, resource, input, text and other heavily used APIs, +then complete the remaining exposed scope. Prioritization changes work order, +not the final denominator. Preserve the declared private/internal boundary. +Mechanical namespace/file indexes need useful navigation, not invented contracts. +Accidentally published include guards and private visibility helpers are filtering +defects; address publication instead of explaining them as consumer features. + +## Establish the contract before editing + +Read the declaration, implementation, callers and relevant tests as needed to +answer the applicable questions below. Check virtual implementations and failure +paths before promising behavior on behalf of an interface. Existing ordinary +comments can hold valuable contracts: verify and preserve their meaning when +converting them into extracted documentation. + +| Entity | Required meanings to consider | +| --- | --- | +| Type/interface | Purpose, supported use, invariants, default/empty state, ownership, copy/move behavior, synchronization and availability. Unions need active-member rules. | +| Function/method/operator | Inputs and defaults, units/ranges, result and ownership, side effects, publication timing, valid state/thread/context, failure and preserved state, and differences between overloads. | +| Constructor/destructor/reset | Acquired and borrowed resources, construction failure, empty/moved-from state, teardown prerequisites, idempotence where promised, and final release versus deferred native retirement. | +| Field | Meaning, units, coordinate/index domain, valid values, sentinel/default behavior, ownership/mutability and relationships to other fields. | +| Enum/value/flags | Purpose of the choice, each value's effect, valid combinations and unsupported/reserved values where defined. | +| Template/alias/concept | Parameter meanings, constraints, additional semantics beyond the spelling, specialization differences and relevant related types. | +| Constant/public macro | Meaning, units/domain, sentinel behavior, feature/build availability, and intended consumer or module-author use. | +| Callback/callable argument | Invocation phase/thread, synchronous or deferred execution, argument/capture lifetime, retained ownership, reentrancy, exceptions and unsubscription. | +| Header/namespace | Include purpose, required link targets/features and route to the owning guide. Document actual choices; avoid filler for a mechanical index. | + +For operations, check these details explicitly when relevant: + +- Null, empty, zero, negative, out-of-range and unknown values; whether input is + copied, moved, borrowed or retained, and how long it must remain valid. +- Return values, references, views, handles, futures and optional results; what + they own or borrow, when they become usable, and what invalidates them. +- Which thread/owner/context may call, what is synchronized, and when accepted + requests become visible. Distinguish request acceptance from applied state. +- Allocation, validation and backend failures; exception types or error results + that callers need, partial effects, retained prior output, and retry behavior. +- Mutations, callback invocation, resource-domain requirements, teardown ordering + and behavior after move/reset/shutdown. State guarantees only when supported. + +Use `@brief`, `@param`, `@tparam`, `@return`/`@retval`, `@throws`, `@pre`, `@post`, +`@note` and `@see` when they make the verified contract easier to retrieve. Omit +inapplicable sections. A parameter entry explains its meaning and restrictions, +not just its C++ type. Document `void` operations through effects and state. + +Do not invent a stronger guarantee than source or an established interface +supports. An implementation defect, ambiguous contract or unsupported expectation +is discovered work requiring resolution, not an invitation to write plausible +prose. Keep internal algorithm commentary separate from public obligations. + +## Keep class definitions compact + +The repository preference is a scannable class body with detailed contracts outside +it. Follow the existing declaration order and keep data members above methods. +Do not move definitions, change signatures/access, or refactor a class to fit docs. + +| Content | Preferred location | +| --- | --- | +| Type overview | Before the class/struct/union, outside its body. | +| Short field or enumerator meaning | After its declaration, using `///<` or `/**< ... */`. | +| Detailed member or method contract | After the complete class, in the same extracted header, using an explicit qualified Doxygen target. | +| Definition already outside its class in an extracted header | Before that definition when it attaches to the intended entity without a repeated target. | +| `.cpp` definition documentation | Only when that source is included in the extraction and cache inputs; the current pipeline excludes it. | + +Doxygen supports trailing member comments and detached structural blocks. Keep +structural commands out of trailing blocks. See the official +[comment placement guide](https://www.doxygen.nl/manual/docblocks.html). + +### Example placement + +This fictional example illustrates locations, not a Cheryl API or a complete +implementation. Adapt contracts to actual behavior; the declaration list remains +short even when the method needs a detailed explanation. + +```cpp +namespace Example { + /** Owns bytes that callers can copy into their own output storage. */ + class Buffer { + public: + std::size_t max_copy = 0; ///< Maximum bytes per copy; zero permits no copying. + + [[nodiscard]] std::size_t copy_to(std::span destination) const; + }; +} + +/** + * @fn std::size_t Example::Buffer::copy_to(std::span destination) const + * @brief Copies buffered bytes, bounded by destination size and max_copy. + * @param[out] destination Writable storage that must not overlap the buffered bytes. + * @return Number of bytes copied; zero when no bytes can be copied. + * + * The caller retains ownership of destination; this operation does not retain it. + * Bytes beyond the returned count remain unchanged. Concurrent mutation of this + * buffer requires caller synchronization. + */ +``` + +If a field needs more than a short trailing explanation, keep that explanation +short and place its additional contract outside the class, for example: + +```cpp +/** + * @var std::size_t Example::Buffer::max_copy + * @details Changing the limit affects later calls, without resizing buffered storage. + */ +``` + +Maintain one detailed contract for an entity. Avoid splitting its rules among +several detached blocks or duplicating the same prose beside the declaration, +definition and manual. A useful short brief and one detailed owner can coexist. + +### Associate detached blocks precisely + +Use `@fn` with a qualified function declaration on one line, and `@var` with the +qualified variable declaration. Match argument types, template scope and relevant +qualifiers, including leading `::` inside template arguments; update targets +with signature changes. Prefer direct attachment when a definition already lives +outside its class. The official +[command reference](https://www.doxygen.nl/manual/commands.html#cmdfn) explains +these targets and their duplication risk. + +Review constructors, conversions, overloaded operators, cv/ref-qualified methods, +constrained templates, partial specializations and inherited methods individually. +Do not create a phantom declaration or attach several overloads' contracts to one +member. Preserve real defaults, qualifiers, attributes and base types in output. + +Describe each overload's actual differences with explicit links. Avoid order-based +"the overload above" wording and automatic `@overload` prose: current extraction +sorts member documentation. Use `@copydoc`, `@copybrief` or `@copydetails` only for +the same contract; identify overloaded targets by argument types. Add actual +override differences instead of copying incompatible lifetime or failure promises. +The current parser can fail on nested template return types; a verified +`@fn auto Qualified::method(...) -> ReturnType` target can attach where the +prefix-return form does not. Inspect XML even when extraction emits no warning. +For the two ref-qualified `StartupResult::make_runtime` declarations, detached +`@fn` matching loses the ref qualifier. Their concise trailing briefs and the +complete type-level lifetime contract provide the tested compact fallback. + +See the [overload](https://www.doxygen.nl/manual/commands.html#cmdoverload) and +[copy commands](https://www.doxygen.nl/manual/commands.html#cmdcopydoc). + +### Respect extraction inputs + +Current `header_inputs()` selects public `.h`, `.hpp`, `.inl`, `.ipp` and `.tpp` +files under the configured include roots, plus the explicitly selected public +exception owner at `projects/engine/include/internals/exceptions.h`. Its historical +path does not authorize extracting other implementation headers. +`SEARCH_INCLUDES` is off and the extraction fingerprint hashes those headers. A comment only in a `.cpp` file will +neither enter the reference nor invalidate its extraction cache. + +Doxygen can associate comments at an out-of-class source definition when that +file is an input. If implementation work adopts `.cpp` placement, treat selecting +the source files, cache invalidation, public/private filtering and source inventory +as one necessary tooling change. Check cold and incremental output and prevent +internal source entities from becoming accidental public articles. Moving comments +alone is insufficient. Keep the header form until the supported path is established. + +Do not add filler file comments just to trigger extraction: the current Doxyfile +uses `EXTRACT_ALL=YES`. Write header documentation when it explains real include, +feature, linking or migration choices. Missing-documentation warnings are disabled; +an undocumented declaration can pass generation. + +## Connect symbols to use + +Give significant APIs a nearby route to the canonical manual section or maintained +example that shows their use. Link precise lifecycle/ownership rules when the +operation depends on them; retain essential preconditions locally. Link related +declarations explicitly with Doxygen references when they clarify a choice. + +Before adding guide links throughout comments, verify one complete path through +Doxygen XML, Breathe and the generated HTML. A Markdown link relative to a header +is not necessarily resolved relative to that header in the final API article. +Use a supported, checked mechanism; keep links usable in the generated reference +and versioned documentation. Avoid guessed generated filenames or a pinned web +URL that disconnects the example from the displayed source revision. + +Use the checked manual-link form, with the document name relative to `docs/site/`: + +```cpp +/** @see [Font selection](cheryl-guide:assets/text-layout#font-selection) */ +``` + +The repository-link extension resolves `cheryl-guide:` after Doxygen/Breathe to +a relative HTML link within the current documentation version. The document must +exist; normal generated-site checks verify its fragment. Use actual headings, +not guessed anchors. This form has a Doxygen-to-HTML pilot; ordinary source-relative +Markdown paths do not gain that interpretation automatically. + +## Verify a batch and finish the scope + +Before bulk comment placement changes, check representative cases: a trailing +field and enum value, a detached method and longer field contract, an overload, +a template/specialization and a virtual/inherited member. Use the actual repository +toolchain and permitted verification. Official Doxygen syntax support alone does +not prove correct association or rendering through Cheryl's full pipeline. + +For each coherent subject batch: + +1. Compare the current extracted/published entities with the source declarations. + Account for every exposed member, including nested and inherited content. +2. Verify comments against implementation, then inspect XML association, parameter + names, overload targets, qualifiers and cross references. Unmatched blocks, + phantom declarations, accidental internal exposure and lost existing prose + are failures even when some output looks correct. +3. Inspect the rendered type/member articles. Confirm that prose, parameter/return + information, specialization bases, signatures, examples and guide links are + accurate and findable. Do not compensate for a wrong generated declaration + by adding a prose disclaimer; fix the owning extraction/rendering mechanism. +4. Walk a starting-reader use question and an expert contract question from the + symbol. Require an explicit answer or useful handoff, not engine-source reading + to fill a documentation gap. +5. Run the applicable permitted checks and generation, using the regular output + described in the [tooling guide](../site/development/documentation.md). Report + exactly what was source-inspected, extracted, rendered and executed. + +Track coverage in the working context against the current entity inventory. +Account explicitly for an intentional exclusion or mechanically sufficient +declaration; do not count it as a fully documented complex contract. Keep the +historical usefulness report intact. A successful build, comment count or average +score cannot establish that every requested API entity has been covered. + +API work is complete only when the full requested exposed scope is accounted for, +applicable contracts are supported and visible, declaration identity is preserved, +and relevant links/presentation checks pass. Unresolved semantic or rendering +defects remain outstanding work at handoff. diff --git a/docs/for-agents/architecture.md b/docs/for-agents/architecture.md new file mode 100644 index 00000000..03b2ae74 --- /dev/null +++ b/docs/for-agents/architecture.md @@ -0,0 +1,127 @@ +# Architecture and design + +Use this guide to decide how a requested capability or correction should fit the +existing system. Produce a source-backed recommendation that an implementation +can follow, including unresolved decisions and acceptance limits. + +Read [shared workflow](workflow.md) for work modes, source authority, scope and +shared-tree boundaries. Use [implementation](implementation.md) after a design +has been selected, and [testing](testing.md) when defining its acceptance. +The engine's subject guides and declaring sources own its actual contracts; +this page supplies a design method rather than another engine architecture. + +## Frame the problem + +State the result the caller needs before choosing an API, class or module: + +- Who performs the operation, and what useful result should they obtain? +- What application work, failure or dependency makes the current path inadequate? +- Which requirements are fixed, and which examples merely illustrate them? +- What must remain compatible, and what is outside this decision? + +Separate the requested outcome from a proposed mechanism. An example involving a +book, HUD or loader does not by itself require an engine-owned book, HUD or loader +policy. Identify the reusable responsibility and the application choices before +deciding where either belongs. + +## Establish the current and intended contracts + +Trace the relevant public entry point through its implementation and cooperating +owners. Read the configuration and relevant tests where they establish selection, +lifetime or failure behavior. Expand investigation to resolve a consequential +uncertainty; avoid collecting an unrelated source inventory. + +Keep these distinctions explicit: + +| Evidence | Record | +| --- | --- | +| Observed implementation | What the current source and configuration actually do, including rejection and cleanup. | +| Documented intent | The requirements or rationale in the owning guide or plan; identify any mismatch with source. | +| Verified behavior | The cases actually observed, with their configuration and limits. | +| Open question | The assumption, missing evidence and consequence if it proves false. | + +Look for an existing operation that already supplies the requested responsibility. +Check its convenience layers and lifetime authority before proposing another +registry, dispatcher, fallback, ownership wrapper or application workaround. +Distinguish a confirmed bug, an absent integration and an optional convenience; +they can require different decisions. + +## Compare viable approaches + +Consider keeping the current contract, extending an existing owner, and adding a +new boundary where those are meaningful alternatives. Explain the consequences +that affect the caller and maintainer, rather than presenting an inventory of +possible abstractions. + +Compare the applicable tradeoffs: + +- Application setup and the ordinary path, including sensible existing defaults. +- Ownership, dependency direction and the ability to use a component independently. +- Lifetime and shutdown obligations, retained data and borrowed access. +- Execution owners, synchronization, publication and progress during waits. +- Failure visibility, recovery, cancellation and preservation of useful state. +- Compatibility, migration effort and the cost of maintaining multiple paths. +- Evidence and fixtures needed to establish the claimed behavior. + +Evaluate an interface by its architectural purpose and contract. Do not remove it +because it has no callers or few callers. Determine which extension, replacement, +independent consumer or supported responsibility it exists to serve. + +Recommend the approach that addresses the actual requirements, with the material +tradeoff and rationale. Do not imply that a broader redesign is necessary merely +because it would simplify the proposed local implementation. + +## Specify the boundary precisely + +Describe a concrete caller sequence and enough API shape to expose the important +decisions. A short example can reveal missing initialization, result adoption or +cleanup that a type diagram hides. Identify proposed APIs as proposed. + +| Contract question | Resolve when applicable | +| --- | --- | +| Inputs and outputs | Valid values, units, defaults, identifiers and returned ownership. | +| Ownership and lifetime | Owners, borrowed targets, retained snapshots, invalidation and destruction order. | +| Execution | Allowed callers, owner threads, synchronization and whether an operation queues, blocks or runs inline. | +| State and publication | Acceptance versus completion, visibility boundaries, ordering and replacement/coalescing behavior. | +| Failures | Synchronous exceptions, asynchronous results, cancellation, partial effects, retry and preserved state. | +| Capability limits | Observable unsupported cases and the guarantees supplied by optional integrations. | + +Trace shutdown as carefully as ordinary success. Determine which producers stop, +which accepted users must finish, who continues servicing dependencies, and when +borrowed objects or native domains can disappear. Retaining a handle does not +automatically retain every resource needed to use it. + +## Check compatibility and acceptance + +Identify affected public declarations, headers, link targets, saved values and +application sequences. Separate source compatibility, binary compatibility and +runtime behavior where they differ. Explain a required migration without silently +changing an established contract to make a new example convenient. + +For build, tooling, SDK or CI boundaries, follow +[producer-to-consumer analysis](implementation.md#trace-build-tooling-sdk-and-ci-changes). +Check the affected configurations rather than assuming one source build answers +installed, relocated or packaged consumption. + +Define acceptance in terms of observable behavior: the normal caller outcome, +material rejection/failure cases, lifecycle boundaries and relevant configurations. +Identify existing coverage that can be reused and the specific missing cases or +fixtures. Use the [testing guide](testing.md) to distinguish proposed coverage +from checks that actually ran. + +## Hand off a decision + +Give the next phase a concise account of: + +- The selected contract and why it addresses the caller's problem. +- The responsible owner, concrete call sequence and affected boundaries. +- Material alternatives rejected and the tradeoff that decided between them. +- Source evidence, assumptions and decisions that remain unresolved. +- A coherent implementation sequence and its dependency checkpoints. +- Required acceptance, observed evidence and outstanding limits. + +Keep open decisions visible. A design recommendation does not establish that the +implementation exists or that its behavior has been accepted. Use the +[shared workflow](workflow.md) for progression and handoff boundaries, and the +[documentation workflow](documentation.md) when maintaining durable design or +subject documentation is part of the task. diff --git a/docs/for-agents/documentation-building.md b/docs/for-agents/documentation-building.md new file mode 100644 index 00000000..d1beb204 --- /dev/null +++ b/docs/for-agents/documentation-building.md @@ -0,0 +1,67 @@ +# Generate and verify documentation + +Use this guide when generation, generated-page inspection or stale-output diagnosis +is part of the authorized task. Read the [shared workflow](workflow.md) and exact +[operation permissions](testing.md#verification-and-resource-use). Full generation +requires authorization; reuse authorization already given for the relevant work. + +The [published tooling guide](../site/development/documentation.md) owns commands, +cache invalidation, tool versions, CMake entry points and output selection. Use +[API documentation](api-documentation.md) for declaration association and +[documentation workflow](documentation.md) for content correctness. Generation +success alone establishes neither. + +## Locate the current inputs and output + +1. Check the current checkout, branch, Git status and affected diffs. Distinguish + authored source, extraction inputs, generated API sources and HTML. +2. Use `docs/site/` as the manual source and `docs/site/generation/` for the + generator/configuration. API inputs come from the generator's explicit inventory. +3. For normal local delivery, generate into + `docs/site/generation/generated/html/`. The entry is `index.html`. Use another + output profile only when it serves the requested verification; it does not + replace the normal local site promised to the user. +4. When diagnosing stale content, inspect the browser path and that output's + `revision.json`. Old folders, other checkouts and cached tabs can show a valid + older site. A file's existence does not establish current generation. + +## Run the relevant generation + +Use the existing pinned/cached tools through the repository wrapper. Inspect +readiness before installing or downloading anything; reuse compatible tools and +caches. Preserve active user invocations and bound agent-run parallelism. +Follow the tooling guide's normal generation, extraction-only and clean-rebuild +choices based on the changed input or observed failure. Do not bypass the owning +wrapper or change unrelated build configurations to make output appear current. + +For authorized normal local generation, from a Git working tree: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" + ./generate-docs.sh --jobs 4 +) +``` + +Agent invocations may use reduced process priority; user-run commands retain +normal priority and appropriate available parallelism. The `--jobs` argument +bounds Sphinx workers, not an Engine build. + +## Verify the delivered artifact + +- Require successful generation and its local link/source checks. Inspect warnings + and failures; do not present an older HTML file left behind as a successful rerun. +- Inspect representative changed page bodies. For API work, follow comments from + source through XML into visible type/member prose, correct signatures and guide + links. The presence of an identifier or anchor alone does not prove prose rendered. +- Compare `revision.json` with the intended source revision and dirty status. For a + final committed delivery, generate/check that revision and confirm the normal + output path. A dirty preview remains a preview and must be described accordingly. +- Check old bookmarks and duplicate generated roots when the user's observation + disagrees with the inspected output. Link the exact current page and explain any + identified stale copy. Preserve user outputs; remove or replace them only within + an authorized cleanup task. +- Report the actual generated entry, revision, inspected content and checks. + Keep source review, example execution and hosted publication acceptance distinct. + Local generation does not publish or verify the hosted site. diff --git a/docs/for-agents/documentation.md b/docs/for-agents/documentation.md new file mode 100644 index 00000000..0c367014 --- /dev/null +++ b/docs/for-agents/documentation.md @@ -0,0 +1,417 @@ +# Write and review useful documentation + +Help the intended reader finish a task or answer a question without guessing. +Start with a plain explanation, then provide the exact rules needed to use the +system correctly. A concise entry and detailed contracts can belong on the same +page when headings and links make each easy to find. + +Use the [shared work rules](workflow.md) for authorization and phase boundaries. +This guide owns documentation audit, authoring and maintenance policy. The +[documentation directory](../README.md) identifies existing owners; the +[tooling guide](../site/development/documentation.md) owns generation commands, +outputs, caches and tool selection. + +## Establish the task and its owner + +Before editing, inspect Git status and relevant existing changes, then identify +the reader's task, the content being changed and its authoritative owner. Choose +the appropriate operation: + +| Operation | Starting action | Required result | +| --- | --- | --- | +| Create | Search existing pages and public declarations for the answer. | Fill a distinct unanswered need in its existing owner, or justify a new page. | +| Improve | Read the whole affected unit, its entry route, relevant source and connected examples. | Repair a specific reader problem while preserving correct contracts and useful detail. | +| Audit | Define the corpus, questions and evidence before judging content. | Report supported findings and limits; follow the repository's audit/maintenance boundary. | +| Maintain | Trace the changed contract or completed work through its owners and dependents. | Reconcile current instructions, examples, links, plans and acceptance scope. | + +Keep the working scope concise: audience, intended outcome, starting scaffold, +affected owners, evidence and completion checks. Record unresolved implementation +questions separately from prose defects. A documentation task must not silently +change an API or invent behavior to make an example easier. + +### Place the answer + +- Keep published prose under `docs/site/`. Edit module references there; neighboring + module READMEs are aliases. Keep agent guidance and repository plans outside the + published source root. See the [source layout](../README.md#source-layout). +- Update the existing owner before adding another explanation of the same rule. + An example may summarize the rule at the point of use and link its full contract. +- Preserve the established navigation, category order and tutorial progression + during content repair. Add useful local headings and contextual routes. A new + page needs a distinct reader task and one navigation parent in the existing + hierarchy; secondary routes use ordinary links. +- Match the page's title, introduction and incoming labels to the question it + answers. Preserve useful paths and section anchors when consolidating content. +- Source comments own declaration-specific API contracts; subject guides own + cross-system explanations and procedures. Follow the + [API instructions](api-documentation.md) for generated reference work. + +### Establish the evidence + +Read the responsible declaration, implementation and relevant tests/configuration +before writing a current contract. Follow a convenience operation far enough to +verify the dependencies on which its promise relies. Tests provide evidence of +specific cases; source and test presence alone do not establish executable acceptance. + +Prefer the supported engine path and defaults when teaching application setup. +If an example requires application-owned plumbing, confirm that no existing engine +service supplies it and explain the actual application responsibility. A missing +integration is discovered engineering work; do not spread an unexplained workaround +across tutorials as though it were the intended architecture. + +## Layer first use and precise reference + +Start with the operation's purpose and normal supported path. Introduce a concept +when the reader first needs it. Follow with meaningful choices/customization and +the exact contracts, limits and failure rules. Give experts headings that lead +directly to those rules without rereading a lesson. + +Keep ownership, lifetime, thread and failure constraints beside the step that can +violate them. Additional depth can live in the linked reference. A plain opening +must not hide a prerequisite, and a precise reference must not require a beginner +to infer the common sequence from a list of types. + +## Choose the page's job + +Before drafting, answer: + +- Who is reading, and what should they accomplish or understand? +- What knowledge, files, configuration or working example do they start with? +- Which existing page owns the answer, and which source establishes the behavior? +- What result would show success, and where should the reader go next? + +Use the role that matches that job. The outlines below are prompts; keep the +parts relevant to the page rather than adding empty sections. + +| Role | Useful outline | +| --- | --- | +| Index | Purpose and reader choice → meaningful destination table or list → deeper routes. Explain differences between nearby choices. | +| Tutorial | Goal and expected result → prerequisites and starting scaffold → staged changes with concepts introduced at use → observe success → cleanup and next lesson. | +| How-to | Named starting point → exact changes and insertion points → expected result → relevant failures and limits → full contract. | +| System overview | Responsibility and type choices → relationships, ownership and execution → small integration route → detailed owners. | +| Conceptual explanation | Reader question → mental model and necessary terms → relationships and consequences → relevant choices → concrete task/reference. | +| Reference | Valid inputs, outputs and units → applicable ownership, lifetime, thread and state rules → failures and limits → examples and authoritative links. | +| Troubleshooting | Observable symptom → evidence that distinguishes likely causes → corrective action → expected observation and unresolved limits. | +| Plan or acceptance procedure | Current readiness → prerequisites and decisions → next action → observable acceptance. Follow [maintenance policy](#maintenance-policy). | +| API comment | Purpose → meaningful parameter/return semantics → relevant preconditions, ownership, execution and failures → owning guide or related declaration. | + +An index succeeds through its choices and destinations. A simple alias or constant +may answer its lookup question through an accurate declaration and one contextual +link. A resource or lifecycle reference usually needs substantially more contract +detail. Examples should solve an actual reader problem. + +### Task or tutorial starting outline + +Adapt this to the size of the operation. A tutorial may repeat the change-and-observe +steps as it introduces concepts; a focused task may need only one such step. + +```markdown +# Perform the operation + +State the result and when the reader needs it. +Name the working example or starting files and required setup. + +## Make the change + +Identify the file, class/member, hook or configuration to change. +Introduce necessary concepts before using them. +Show the change and explain why it produces the result. + +## Observe the result + +Describe an observable success condition and relevant failure behavior. +Include cleanup when the operation creates a lifetime to manage. + +## Continue + +Link the next task and the authoritative detailed contract. +``` + +An intermediate lesson still needs a checkpoint: a defined return value, state +change or other inspectable result. Say which later lesson makes it visible in +the application. + +### Reference starting outline + +```markdown +# Name the interface or contract + +Explain its purpose and when to choose it. Link the first-use sequence. + +## Inputs and results + +Define valid values, units, defaults and returned ownership. + +## Ownership and execution + +State relevant lifetimes, allowed callers/threads and publication boundaries. + +## State, failures and limits + +Describe transitions, rejection, preserved state and unsupported cases. + +## Examples and sources + +Give a focused example where it helps; link deeper usage and declaring sources. +``` + +For generated reference pages, use the [API instructions](api-documentation.md) +for comment placement, complete entity coverage and source-to-render checks. + +## Start plainly, then give the exact contract + +- Put the reader's answer in the opening paragraph. State what the operation + accomplishes before describing its internal machinery. +- Define necessary terms at first use. Keep names consistent across prose, + snippets, assets and linked examples. +- Give each paragraph one main point. Prefer concrete subjects and verbs over + vague claims such as "handles everything" or "works automatically." +- State units, defaults and important boundaries explicitly. For example, identify + whether a width uses logical coordinates or framebuffer pixels. +- Give snippets their insertion context: file, member declarations, hook, required + includes, configuration and asset prerequisites as needed. Identify placeholders + and distinguish a complete example from a fragment. +- Make the simple path easy to follow, then expose exceptions and exact rules + under descriptive headings. Keep a failure or lifetime rule beside the step + that needs it, even when the full explanation belongs deeper. +- Describe the expected observation. "It builds" and "the requested behavior + works" are different outcomes. +- Preserve useful expert detail. Improve its introduction, ordering or retrieval + when readers struggle to find it; remove repetition after preserving its unique + requirements in the authoritative owner. + +Use short, concrete headings and task labels. Tables help readers choose between +paths or compare rules; use columns that support that decision. Avoid repeated +promises such as "safely" without stating the actual constraint. Use a diagram +when it clarifies a relationship, ownership boundary or sequence; accompany it with +the essential rule in text and confirm the generator can render its format. +Use descriptive link text and language-tagged code fences. Explain meaningful +images/diagrams in text and label units and relationships; color alone must not +carry a required distinction. Check wide tables and long signatures in the +rendered page instead of assuming they remain readable. + +## Make examples and procedures complete + +- Name the initial working scaffold, source/SDK selection and relevant options. + Identify exact files, declarations, members, hooks, includes, link targets and + asset prerequisites. Show where a fragment belongs and what existing code it + replaces or retains. Do not present a fragment as a complete application. +- Follow the full relevant lifecycle: setup, use, observation and release. A lesson + ending in prepared state must identify how that state is inspected and which + lesson makes it visible. +- Prefer maintained example code or includes when they serve the lesson. Check + the lesson's conceptual explanation against that code; inclusion alone does not + establish that the reader understands the missing integration. +- Keep identifiers, font-group names, assets, actions, options and defaults + consistent across linked lessons and references. Check an example's requested + identity as well as its plausible output when fallback can hide a mismatch. +- Explain whether paths are relative to the checkout, asset root, executable or + current directory. Command blocks establish their working directory and preserve + the caller's starting directory. Follow existing repository command conventions. +- Separate ordinary success from expected rejection/failure examples. Explain + what the reader observes, what remains valid and how to recover. Diagnostics + should lead to an action rather than repeat internal implementation names. +- State supported configurations and necessary fixtures at the point of use. + Validate selected platform, static/shared linkage, packaging and runtime claims + along their affected producer-to-consumer path. Do not infer all configurations + from one successful example. + +## Support the claims + +Add technical detail when it helps the reader choose, use or verify the system. +Check that precision against the responsible source. Unsupported specificity +creates an expectation the documentation cannot justify. + +For each consequential statement, identify the source that establishes it: + +| Claim | Evidence to inspect | +| --- | --- | +| Current behavior | Declaring interface and relevant implementation, including failure paths. | +| Selection, linking, installation or deployment | The configuration owner and the affected producer-to-consumer path. | +| Intended change | The owning design or plan, clearly distinguished from current behavior. | +| Accepted behavior | Results for the stated revision, configuration and coverage, with unresolved limits retained. | + +Use precise scope: who owns a value, what a view borrows, when a change becomes +visible, which thread may perform an operation, and what failure leaves intact. +Follow a convenience API through its dependencies before promising behavior on +their behalf. + +If evidence is incomplete, state what is known and what remains uncertain. Label +an inference and identify the missing check. A declaration's presence establishes +that it was extracted; inspect the rendered signature and meaning before claiming +accurate or complete API documentation. + +Keep one authority per fact. Other pages can summarize enough to make a decision +and link the exact contract. Check linked examples together: a name mismatch can +silently select a valid fallback and produce a plausible result. + +For punctuation in headings, distinguish the Markdown heading slug from the +final HTML ID. MyST can resolve `#shadermaterial-definitions` to HTML +`#shader-material-definitions`; copying the latter into a Markdown fragment can +produce an unresolved reference. Check the source form through Sphinx. API +`cheryl-guide:` links enter after that resolution and need the final HTML +fragment. Explicit labels preserve old HTML bookmarks, but a Markdown +`page.md#fragment` still needs that page's recognized heading slug; use the +current heading or a label reference. + +## Review and audit + +For a documentation audit, inspect the relevant implementation and documentation +for inaccurate, obsolete, missing, duplicated or unnecessary information. Report +findings and recommended changes without modifying documents. Distinguish intended +behavior from a mistaken claim about current implementation. + +### Apply existing findings + +The [usefulness report](../../reports/documentation-usefulness/report.md) and its +per-page inventories describe a recorded baseline. Recheck each finding against +the current source and content before using it as a task. Do not overwrite the +historical scores with assumed improvements or treat an old gap as proof that +the corresponding source/test is still absent. + +Prioritize reader harm: false instructions and declaration errors; blocked task +handoffs and inconsistent examples; missing API contracts and guide routes; then +entry explanations, retrieval and scaffolds. Use the requested implementation +sequence when work is divided into stages. A high-scoring page can still contain +a consequential defect. Work order does not narrow a request to cover all content. + +Repair a coherent subject or reader route, including its affected examples and +links, then review it before expanding the next batch. State the problem repaired +and its evidence; a higher score, longer comment or larger page is not the outcome. + +### Bound a new audit + +- Establish the requested audience and corpus before scoring. For a published-site + audit, use the generator's source root, exclusions and include relationships to + identify the rendered content. A repository link does not make its target a + published page. +- Count included canonical content once. Keep manual pages and generated + declaration articles in separate summaries, and put any repository-only review + outside the primary results and priorities. +- Record the source revision and distinguish current observations from a previous + report's findings. Use the existing navigation to find each page's job, entry + context and deeper owners. +- Identify the expected improvement for the reader. A higher score should reflect + a repaired instruction, supported contract or clearer route to an answer. +- Read every substantive unit claimed as reviewed, including relevant member + documentation, rather than extrapolating from a sample. Distinguish complete + reading, selected claim checks and automated measurements. Identify uncertainty + and evidence limits for claims that were not checked. + +### Measure usefulness + +Assess the page for its role and intended reader: + +| Dimension | Question | +| --- | --- | +| Purpose and place | Does the page answer the expected question from its stated prerequisites? | +| Beginner entry | Are the concepts and common path understandable to its intended first reader? | +| Practical usefulness | Can the reader finish the operation or answer the lookup question? | +| Contract depth | Can an expert verify the relevant inputs, ownership, execution, state and failures? | +| Currency and evidence | Do important claims match current source and distinguish implementation from acceptance? | +| Retrieval | Can readers scan to the next step or exact rule? | +| Connections | Can readers find the page, continue the task and reach its deeper owners? | + +Use the report's [rubric and weights](../../reports/documentation-usefulness/report.md#rubric) +when numeric comparisons are requested. Preserve its role-sensitive interpretation +and cap for materially inaccurate content. Keep manual, generated API and +repository-only results separate. Comment counts, link counts and automated +readability measures are evidence screens; they do not establish semantic quality +or replace a reader walkthrough. + +## Review the reader's path + +Use the [usefulness rubric](../../reports/documentation-usefulness/report.md#rubric) +and [assessment method](../../reports/documentation-usefulness/report.md#2-how-the-assessment-was-made) +to structure judgment. The report's findings describe its recorded baseline; +check current source before carrying them into new work. Word counts, comment +presence and fine score differences do not establish usefulness. + +Walk through two questions appropriate to the page: + +1. **Starting reader:** Begin at the stated prerequisites. Follow the links and + instructions to the result. Record missing setup, inferred steps, inconsistent + names and unnecessary detours. Can the reader recognize success? If a fallback + can hide a mistake, check the selected identity or state as well as appearance. +2. **Experienced reader:** Find an exact answer about a relevant input, owner, + lifetime, thread, transition or failure. Can the reader cite the rule directly + or reach its authoritative owner through a clear link? + +Distinguish a static walkthrough from an executed example or observed user task. +Report what was inspected or run, the evidence it provides and remaining limits. +Use the existing verification rules to choose permitted checks. An unexecuted +example needs an honest status, not an implied successful result. + +### Check discovery + +Start from likely task words, the relevant overview and an API symbol reached +through search. Can each route reach the capability and its complete procedure? +Check whether the documentation answers what the engine supplies, what the +application chooses and which integration point to use. Follow the actual route; +a destination's existence is not evidence that a reader can find it. + +For a gap, identify the reader's question, entry point, route attempted, missing +answer and smallest useful correction. Prefer a clear label, contextual link or +missing section in the existing owner over a duplicate page or navigation redesign. + +## Maintain, consolidate and retire + +After a contract changes, follow its impact through API comments, the owning guide, +summaries, examples, procedures and inbound links. Update the affected meanings +together; retain valid detail outside the change. Distinguish observed behavior +from intended changes when source and documented architecture disagree. + +Before removing a document or section, identify its unique requirements, rationale, +procedures and unresolved acceptance. Preserve still-useful material in an existing +owner, then repair incoming links and useful anchors. Completed implementation +history belongs in Git; current contracts belong in references and unfinished work +belongs in its owning plan. Follow the [maintenance policy](#maintenance-policy) below. + +Keep `STATUS.md` as the overview of capabilities and planned work. `planning/` +holds actual plans and an index of those plans. Reconcile both directions: each +actual plan needs a STATUS route; STATUS topics without a concrete plan remain +explicit scoping gaps. Separate ready acceptance in the testing queue from +unimplemented regressions and deferred fixture/device prerequisites. + +### Maintenance policy + +Documentation should help a reader understand the current system, its intended direction, or the work required to reach that direction. + +Prefer updating and consolidating existing authoritative documents over creating new ones. Remove obsolete, duplicate, or superseded documentation when its useful information has been preserved. Do not expand the document collection merely to record intermediate work, reasoning, or implementation progress. + +Apply these rules to affected documents across `docs/` during authorized maintenance: + +- Describe current contracts in the present tense. Keep plans focused on the target state, unresolved work, prerequisites, ownership/dependency constraints, ordered steps, discovery boundaries and acceptance criteria. +- Keep one concise progress checklist for each active macro task in the plan that owns its details. Reconcile accumulated subtask progress during maintenance, retaining checked subtasks while the macro task remains open. Keep future macro tasks at roadmap detail until they become active; link to the owning checklist instead of maintaining duplicate progress lists. +- Remove completed macro-task narratives and superseded alternatives. Document resulting durable behavior in its authoritative subject document, and link to it instead of duplicating contracts or source inventories. +- Preserve unique requirements, decisions and rationale that still guide implementation. Before shortening or deleting a plan, verify that each remains available in the surviving text or a linked authoritative document. Do not replace relevant detail with a vague summary or a pointer to Git alone. +- Git commits and diffs hold implementation history. A brief decision/history summary with specific Git references is useful when it explains the current direction or saves a future reader substantial reconstruction; do not reproduce the task-by-task journal. +- Do not append routine successful build/test/check reports, pass counts, transcripts or toolchain snapshots unless explicitly requested. Retain reusable validation procedures and meaningful coverage limits. Distinguish source completion from executable acceptance; a successful exit with skipped or unselected checks does not establish the missing coverage. +- Record unresolved failures, unavailable prerequisites and environment/compatibility limits only to the extent they affect future work, with the required next action. Remove resolved blockers unless they describe an enduring constraint. +- Reconcile affected plans and current-state documentation against accumulated completed work. When a macro task completes, remove its task section and checklist after preserving any reusable procedure, durable contract or still-relevant decision rationale and repairing inbound links. Delete a one-purpose plan when no unresolved work remains. Do not move its execution log elsewhere merely to empty the plan. + +Keep a paragraph when it explains how something works, how it should work, how to get there, or a decision/constraint needed to do so correctly. Concision must preserve that information. + +Historical audit data retains its reviewed revision and findings. Repair live +report links when owners move; use revision links for retired evidence. A later +audit records its new scope and evidence rather than silently rewriting history. + +## Complete a documentation unit + +Before handing off, establish the following for the affected scope: + +- The intended reader can find the answer, follow the normal path and recognize + success; experts can retrieve precise rules and their source. +- Consequential claims, names, examples and configuration boundaries agree with + the responsible source and connected documents. +- The change preserves useful contracts, established navigation and relevant + unresolved work; it introduces no competing authority or unexplained workaround. +- Applicable links, targets and fragments resolve. Inspect the rendered result + for presentation-sensitive changes and API work; source Markdown or XML alone + does not prove usable HTML. +- When generation is part of the authorized task, use the regular output at + `docs/site/generation/generated/html/`, following the tooling guide. Record the + actual output and revision; do not substitute a build-directory copy. +- Report changed content, checks actually performed and outstanding evidence or + decisions. Generation/link success does not prove that prose is accurate or + that the documented application procedure ran successfully. diff --git a/docs/for-agents/git.md b/docs/for-agents/git.md new file mode 100644 index 00000000..91eb636e --- /dev/null +++ b/docs/for-agents/git.md @@ -0,0 +1,109 @@ +# Git work and branch consolidation + +Use this guide when creating commits or integrating branches. The shared +[workflow](workflow.md) owns task authorization, progression and shared-tree +safety. A review of branches remains read-only until integration is requested. + +## Create coherent commits + +Complete and commit coherent development units independently after the relevant +permitted checks. Commit only task-related changes that can be separated cleanly +from pre-existing work. Inspect status, affected diffs and the staged diff before +committing; do not stage unrelated or unfinished user changes. + +If task work and pre-existing work cannot be separated safely, leave the overlap +uncommitted and explain the boundary. Do not rewrite the user's work to manufacture +a clean commit. A commit records the completed source change; state any outstanding +executable acceptance separately. + +For agent-created commits, supply this identity on the Git command: + +```sh +git -c user.name=cppcooper \ + -c user.email=cppcooper@users.noreply.github.com \ + commit --file /path/to/reviewed-commit-message +``` + +Do not change global or repository Git configuration. The message begins with a +third-person singular present-tense verb, such as `Adds`, `Updates`, `Revises`, +`Deletes`, `Fixes` or `Requires`. Use a detailed body appropriate to the development +unit: the resulting behavior, affected contracts and meaningful verification +limits. Preserve actual newlines when passing a message through a file or a +structured tool argument. + +Preserve individual completed commits normally. Do not amend, squash or otherwise +rewrite them without an explicit request. Do not generate mailbox patches or +push commits/branches to a remote unless explicitly requested. A request to merge +locally does not authorize a push or branch deletion. + +## Consolidate branches into fixes-develop + +The established policy for a user request to merge completed branches down into +`fixes-develop` is a **squash consolidation**, with every integrated original +commit's full message retained in the aggregate commit body. The user explicitly +confirmed this strategy; it is the exception to the normal individual-commit rule +for this destination. Do not apply it automatically to another destination or to +an unrelated integration request. + +The consolidation creates a new aggregate commit on `fixes-develop`. Keep the +original commits and source branch refs; do not rebase, amend, reset or delete +them as part of this operation. `develop` and `main` remain unchanged unless the +user separately requests their update. + +### Establish the integration set + +1. Inspect status and relevant diffs. Require a clean index and working tree for + the consolidation; resolve or safely separate authorized task work first. + Do not discard or automatically stash another person's work. +2. Inspect local branch tips, tracking refs and worktrees. Record the full + destination/source commit IDs before switching branches. Preserve active + worktrees and their outputs; do not force a checkout of a branch used elsewhere. +3. Check ancestry and divergence, including whether `develop` and `main` are + already ancestors. A detached review worktree is not another branch to merge. + Local tracking refs describe the last fetched state; do not claim they prove + the current remote state. +4. Determine the distinct commits being integrated relative to the destination. + For stacked branches, count each shared ancestor once. Do not concatenate a + separate overlapping log for every branch. The union from + `git rev-list --reverse --topo-order --not ` identifies + distinct reachable commits; retain their full messages, not just subjects. +5. If the stack is linear and all selected branch tips are ancestors of one final + tip, integrate that final tip once. This includes the intermediate branches + without repeated squash operations. Include an authorized user commit at that + tip as part of the requested branch contents; do not silently drop it. + +An older squash may represent changes without preserving ancestry from its +source branch. Establish whether those changes are already integrated rather +than replaying a historical snapshot merely because it is not an ancestor. +Resolve routine integration details within the requested scope; seek direction +when source histories or conflicting requirements leave a material choice unclear. + +### Produce and verify the aggregate + +Prepare the complete message in a temporary file before integrating. Start with a +summary such as `Merges completed branches into fixes-develop (squash)`, then include +each distinct integrated commit's full message in chronological/topological order. +Commit IDs may identify the entries. Preserve their text and original message +bodies; do not replace them with a newly written summary of the work. + +Switch to `fixes-develop`, squash the reviewed integration tip or tips, and inspect +the staged result before creating the aggregate with the identity above. Do not +treat a merge conflict as permission to overwrite pre-existing work or silently +choose between incompatible contracts. + +For a linear stack whose destination is an ancestor of the selected final tip, +the committed aggregate tree must equal that tip's tree. Compare their tree IDs +or use `git diff --exit-code fixes-develop`, then confirm clean +status and that the source refs still point to their recorded commits. Squashing +does not make those original commit IDs ancestors of the destination, so +`git branch --merged` alone cannot establish consolidation success. + +An unchanged final tree does not require another native build merely because the +commit shape changed. Report the tree comparison and relevant prior verification +accurately. If conflict resolution changes content, assess the affected contracts +and run the smallest checks permitted by the [testing instructions](testing.md); +do not infer executable acceptance from a clean Git merge. + +Report the new destination commit, source tips included, preserved original refs, +tree-equivalence result and any unresolved verification. Keep the result local +unless the user requested a push. diff --git a/docs/for-agents/implementation.md b/docs/for-agents/implementation.md new file mode 100644 index 00000000..636fe34a --- /dev/null +++ b/docs/for-agents/implementation.md @@ -0,0 +1,139 @@ +# Implementation, refactoring and tooling + +Use this guide to turn an agreed outcome or design into a coherent development +unit. It covers implementation planning, producer-to-consumer changes and Cheryl +code conventions. The [shared workflow](workflow.md) owns scope, source authority, +authorization, repository safety and shared-work boundaries. + +Use [architecture and design](architecture.md) for unresolved contract decisions, +[testing](testing.md) for test work and verification, [Git](git.md) for commits, +and [documentation](documentation.md) for a documentation phase. + +## Prepare a coherent development unit + +Before nontrivial implementation, establish a concise plan containing: + +- The requested outcome and the behavior or contract being changed. +- Coherent tasks that can be completed and reviewed together. +- Risky assumptions and the evidence needed to resolve them. +- Dependency boundaries and any required decision checkpoints. + +Keep the plan in the working context unless a persistent planning artifact was +explicitly requested. Resolve foundational decisions and uncertain boundaries +before accumulating dependent code. Follow the plan within its approved scope; +revise it when implementation reveals material new information. + +Identify the current owner of the behavior and trace its callers and dependencies +far enough to understand the change. Read the relevant implementation, tests and +configuration rather than treating a public declaration as its complete behavior. +Use the [architecture guide](architecture.md#specify-the-boundary-precisely) when +ownership, execution, failure or compatibility needs a new decision. + +## Implement the complete behavior + +Make the approved design concrete through its relevant lifecycle: construction, +ordinary use, state transitions, failure, cleanup and the affected consumer path. +Keep application policy and reusable responsibilities with their selected owners. +Preserve the established contract outside the requested change. + +Necessary investigation, affected test maintenance and focused regression coverage +belong with the behavior being implemented. Follow the [testing guide](testing.md) +for their design, execution and evidence limits. Address material failure paths +as well as the straightforward success case. + +Add essential comments and runtime logging required by the implementation at the +relevant operation. Keep broader comment cleanup, unrelated logging improvements +and explanatory documentation with their own objectives. The +[documentation workflow](documentation.md) owns synchronization and maintenance. + +## Trace build, tooling, SDK and CI changes + +### Establish the producer and consumer contract + +Before changing behavior, identify the affected contract, its owner and the path +from producer to consumer. Follow the portions relevant to the task through: + +1. Artifact creation and the inputs/configuration that select it. +2. Installation and packaging, including dependency ownership. +3. Relocation and runtime discovery/loading. +4. Cache reuse and invalidation at the affected boundary. +5. Workflow activation and the prerequisites that make the path run. + +An artifact being created does not establish that it was installed, shipped, +found after relocation or used by the intended consumer. Inspect only the affected +path; broad inventories do not replace tracing that contract. + +### Extend the existing policy owner + +Locate the existing authority for each policy before adding another implementation. +Prefer extending that owner over duplicating policy in a wrapper, classifier, +cache rule, fallback or compatibility branch. + +When CMake, Python or workflow code must express the same policy, keep their +semantics aligned and cover their agreement with focused checks. Check both the +decision and the context in which it is evaluated; identical-looking expressions +can receive different inputs at configuration, installation or script execution. + +### Select the affected execution contexts + +Choose coverage according to the changed boundary, including relevant differences: + +| Context | Question | +| --- | --- | +| Project configuration versus generated CMake install/script execution | Are values, paths and available targets valid at the time the code runs? | +| Source versus installed SDK consumption | Does the installed consumer receive the promised headers, targets and dependencies? | +| Static versus shared linkage | Are dependency closure and runtime loading correct for the selected linkage? | +| Linux versus Windows | Are platform paths, binaries and native requirements handled by the responsible owner? | +| Cold versus warm caches | Can fresh production and retained artifacts both follow the intended policy? | +| Default-branch workflow prerequisites | Will the intended event, branch and upstream artifacts actually activate the changed workflow? | + +Use the relevant subset; do not run the entire matrix for every edit. Follow +[testing and verification](testing.md) for permitted checks, resource use and +reuse of compatible builds/caches. + +### Preserve the contract when repairing a check + +Trace dependency ownership before changing bundling rules or system-library +exceptions. Distinguish correcting a verifier from changing what the SDK promises +to supply. A failing check is evidence to investigate, not permission to weaken +the producer or consumer contract. + +For repeated failures, revisit the common assumption before growing special cases. +Remove superseded paths within the requested unit; keep a broader redesign as +separate discovered work. + +## Follow Cheryl conventions + +- Follow the repository's existing style and `.clang-format`. +- Put data members above methods when organizing class declarations. +- Keep unit-test names short and intuitive. +- Exercise behavior without intentionally emitting compiler warnings in normal + test builds. Consume results that matter to an assertion; explicitly discard an + irrelevant `[[nodiscard]]` result with `static_cast(...)`, including inside + exception assertions. Apply the same discipline to other intentional diagnostic + triggers. +- Test compiler diagnostics in isolated probes that capture and assert the expected + diagnostic. Keep suppression scoped to that construct and compiler. Preserve API + attributes and target/project warning settings rather than weakening them to + accommodate a test. +- Evaluate interface changes through their + [architectural purpose and contract](architecture.md#compare-viable-approaches), + including supported extension and replacement responsibilities. + +## Review, verify and commit the unit + +Review the resulting behavior and consumer path against the agreed outcome. +Check that a local repair has not shifted ownership, changed unrelated guarantees +or introduced a second policy authority. Use the smallest relevant permitted +checks and maintain the directly affected coverage through the +[testing guide](testing.md). + +Complete and commit coherent development units independently after appropriate +permitted checks, following the [Git guide](git.md). State outstanding executable +acceptance explicitly; a commit does not establish that an unrun native, installed +or runtime path has succeeded. + +At handoff, describe what changed and why, the relevant contract, evidence actually +observed, and remaining verification or decisions. Preserve discovered testing +needs for the [testing-request process](testing-requests.md), and use the +[documentation workflow](documentation.md) for any authorized documentation phase. diff --git a/docs/for-agents/investigation-and-planning.md b/docs/for-agents/investigation-and-planning.md new file mode 100644 index 00000000..99b5d6ce --- /dev/null +++ b/docs/for-agents/investigation-and-planning.md @@ -0,0 +1,91 @@ +# Investigate and plan work + +Use this guide for review, investigation, research and working plans. The shared +[workflow](workflow.md) owns work-mode authorization, progression, source authority +and shared-tree safety. This guide supplies the method for establishing an answer +or a concrete next step within that scope. + +## Frame the question + +Identify the requested outcome and the decision the investigation must support. +State what is being reviewed, which contracts or owners may be affected, and what +evidence would answer the question. Read-only investigation does not authorize +implementation when a fix becomes apparent; retain that fix as a recommendation. + +Acquire context purposefully. Start with the relevant declarations, +implementations, configuration and concrete consumers. Follow dependencies far +enough to establish the affected contract, rather than loading the whole project +or every plan by default. Consult the subject guide, roadmap or history when it +explains intended behavior or a still-relevant constraint. + +When the user or another agent is changing the same source, establish what revision +the observations describe. Use a stable existing snapshot or commit-based reads +where appropriate; do not turn a transient mixed read into a claim about a coherent +implementation. Record relevant uncertainty and preserve ongoing work. + +## Trace the contract and evidence + +Find the owner and follow the path from caller to result, including applicable +ownership, lifetime, execution, failure and cleanup boundaries. Check whether an +existing API or supported composition already solves the apparent problem before +recommending another abstraction or application workaround. + +Distinguish these findings in the working notes and handoff: + +| Kind | State | +| --- | --- | +| Observed implementation | What the current source/configuration does, with a concrete path and location. | +| Documented intent | The requirement, design rationale or planned direction, identified as such. | +| Verified behavior | What an actually executed check establishes, with its configuration and limits. | +| Inference or uncertainty | The reasoning, missing evidence and consequence for the decision. | + +Source inspection can establish a code path or declared contract; test presence +does not establish a successful run. If implementation and documented intent +disagree, explain both rather than silently treating either as correct. Expand +the investigation when a dependency invalidates the current assumption, and use +the shared workflow's boundaries when further work needs user direction. + +## Form a working plan + +When the investigation produces a plan, make the next work concrete without +accumulating implementation before its foundations are settled. Include: + +- The intended result, scope and affected owner/contract. +- Coherent tasks in dependency order, with independent work identified where useful. +- Risky assumptions, unfinished-work dependencies and decisions that must be settled + before accumulating dependent changes. +- Applicable completion and acceptance criteria, including the smallest relevant + checks and any unavailable or unauthorized executable verification. +- Mandatory decision checkpoints from the user or approved plan. + +Resolve foundational decisions before extending their dependency surface. Revise +the plan when evidence invalidates an assumption; explain material changes and +their effect on scope or acceptance. Routine implementation choices within the +agreed scope do not each require another approval. A plan itself does not authorize +an operation or an independent objective outside that scope. + +Keep plans and phase handoffs in context unless a persistent artifact is explicitly +requested. For an authorized durable plan, follow the existing plan owner and the +[documentation-maintenance rules](documentation.md#maintenance-policy). +Do not create a new report or planning file merely to record intermediate progress. + +For a design decision, continue with [architecture guidance](architecture.md). +For authorized code changes, the +[implementation preparation](implementation.md#prepare-a-coherent-development-unit) +owns the required plan and development-unit sequencing. +Read the [testing instructions](testing.md) before executing unfamiliar checks. + +## Hand off the result + +Give a concise answer supported by concrete source locations and relevant +consumers. Report findings and recommendations in review/investigation mode; +distinguish them from changes actually implemented. For a material problem, explain +its consequence, existing owner, smallest coherent correction and any decision +that requires user direction. Do not manufacture findings to fill a target count. + +Carry forward established decisions, constraints, outstanding work and affected +contracts. State the evidence collected, verification limits and recommended next +action so the next phase can proceed without reconstructing the investigation. +Stop at the requested objective or mandatory decision boundary; do not begin an +independent implementation, audit or documentation phase on the strength of a +recommendation alone. diff --git a/docs/for-agents/testing-requests.md b/docs/for-agents/testing-requests.md new file mode 100644 index 00000000..17a6d249 --- /dev/null +++ b/docs/for-agents/testing-requests.md @@ -0,0 +1,30 @@ +# Testing-request maintenance + +This guide governs maintenance of [the user-facing testing queue](../testing-requests.md). +It does not contain the queue itself. Use the [workflow](workflow.md) for task +scope and progression, [testing and verification](testing.md) for operation +permissions and evidence, and [documentation](documentation.md) for general +documentation maintenance. + +Testing-request documentation is maintained only when explicitly authorized. Do not update `docs/testing-requests.md` automatically during implementation, refactoring, verification, or other non-documentation phases. + +Preserve discovered testing requirements in the task handoff until a testing-request maintenance phase is authorized. At that point, reconcile accumulated requirements against existing requests. + +These rules govern testing-request documentation. They do not defer directly related test maintenance or permitted verification from an implementation task. + +During authorized testing-request maintenance: + +- Maintain `docs/testing-requests.md` as the user-facing queue of testing needed for implemented work. Reconcile accumulated testing needs with the current source, targets, configuration, reported results, and required coverage. +- Keep a summary table near the top with exactly three columns: **TR#**, **Short description / title**, and **Long description (plain language)**. Give every active request one row, with its `TR` linked directly to its detailed section/heading. Include the same ID in that heading. Preserve request IDs when updating requests; do not renumber remaining requests or reuse retired IDs. +- Prefix each summary-table **Short description / title** cell with **Automated:** or **QA:**, matching the request type in its detailed section. +- Write each table's long description for a reader unfamiliar with engine or testing internals: explain what will be checked, why it matters, and any setup requirement or blocker affecting whether they can run it. Explain necessary jargon; keep exact commands and detailed acceptance criteria in the linked section. +- Aggregate deferred testing across development cycles. Extend an existing request when it covers the same behavior and configuration; avoid duplicate case runs through owner and aggregate runners, and batch needed build targets into as few invocations as practical. +- Label each request as **Automated** or **QA**, with its platform, prerequisites, readiness and acceptance scope. Keep development progress in the owning plan rather than duplicating its macro-task checklist here. +- For each automated request, provide a runnable fenced command block. Include required CMake configuration, explicit build targets, environment opt-ins, working directory and test selection. State when a matching existing build can be reused. +- Command blocks must establish their working directory explicitly, including QA launch blocks. Commands launched from a document may inherit that document's directory; locate the checkout root rather than assuming the shell already runs there, and preserve the caller's starting directory. +- User-run command-line CMake configuration must explicitly select Ninja with `-G Ninja`. Build and test commands use that configured build directory. +- For each QA request, provide a brief detailed summary and concise requirement bullets covering setup, actions, expected observations and required platform/runtime modes. +- Mark requests blocked when fixtures, hardware or an observation harness are unavailable, and identify the next action. Do not invent commands or count skipped, unselected or device-free checks as the missing acceptance. +- When the user reports results, reconcile requests and owning plans during the next authorized maintenance phase against the actual tested revision, configuration, platforms and coverage. Remove accepted requests, preserve unresolved portions and repair affected links; retain durable coverage limits in the relevant subject guide rather than appending routine execution logs. + +Submitting a testing request does not authorize operations that require explicit permission under [Verification and resource use](testing.md#verification-and-resource-use). diff --git a/docs/for-agents/testing.md b/docs/for-agents/testing.md new file mode 100644 index 00000000..dd832bc3 --- /dev/null +++ b/docs/for-agents/testing.md @@ -0,0 +1,74 @@ +# Testing and verification + +Use this guide for test design, test implementation, test execution and +verification. The [workflow](workflow.md) owns task scope, authorization and +progression between independent objectives. [Implementation](implementation.md) +owns code conventions, test naming and compiler-warning discipline. + +The [testing and validation guide](../site/development/testing.md) maps repository +runners and procedures. Maintaining the user-facing testing queue is a separate +objective governed by [testing-request maintenance](testing-requests.md). + +## Choose the objective and evidence + +| Objective | Work | Handoff | +| --- | --- | --- | +| Test design | Read the contract, implementation and existing coverage. Identify observable outcomes, relevant failure paths, prerequisites and the smallest cases that distinguish correct behavior. | Proposed cases, expected results, fixtures and coverage limits. A design is not implemented or executed coverage. | +| Test implementation | Update affected cases and shared fixtures; add focused regression coverage where needed. Follow the implementation guide's naming and warning rules. | Cases changed and the behavior they exercise, with any permitted checks actually run. Written cases alone do not establish acceptance. | +| Test execution | Inspect the selected runner and its subprocesses, confirm the authorized operation and compatible configuration, then run the focused selection. | Actual revision, configuration, selection, results, skips and unavailable prerequisites. | +| Verification | Trace the affected contract through its owner and consumers, choose appropriate evidence, and compare observed results with the required outcome. | Confirmed behavior or failure, inferred cause, and remaining acceptance needed. | + +For supporting regression work within an implementation, apply the workflow's +existing authorization and progression rules. Keep supporting work within that +development unit; an independent testing campaign follows the workflow's task +boundaries. + +## Select relevant coverage + +Choose cases from the changed contract and its failure/ownership boundaries, +then inspect existing owner and aggregate runners before selecting targets. +Prefer an existing case or fixture when it exercises the required behavior. +Trace the operation far enough to identify what it actually checks; an aggregate +name alone does not establish which cases run. + +For build, tooling, SDK and CI work, use the producer-to-consumer analysis and +[affected execution contexts](implementation.md#select-the-affected-execution-contexts) +from the implementation guide to choose relevant configurations and consumers. +Tie each selected check to the boundary it verifies. + +A mocked command or fixture can establish orchestration without establishing +that the generated command succeeds or its output can be consumed. Select +artifact, relocation, runtime or device evidence when that is the claim being +verified, subject to the authorization rules below. + +## Verification and resource use + +- Run the smallest relevant permitted checks before handing off an implementation. Update affected tests with the behavior they cover, and add focused regression coverage when existing checks do not adequately exercise a material change. +- Static analysis and focused script or tooling tests using mocks or disposable fixtures are permitted without separate authorization when they do not compile project or dependency code, run native project executables, install or download tools or dependencies, generate full documentation, or access devices or external services. CMake script and configuration probes that enable no compiled languages may qualify. +- Inspect an unfamiliar check before executing it, including the subprocesses it launches. A Python filename or a description such as "tooling check" does not establish that a check is lightweight. Configuration that invokes compiler or linker probes requires build authorization. +- Project or dependency builds, native test execution, full documentation generation, dependency or tool downloads or installation, and tests using devices or external services require explicit authorization. Existing authorization persists for relevant fixes and reruns within its scope. When additional authorization is necessary to establish the requested outcome, identify the missing verification and request it together rather than waiting for an unrelated blocker. +- Batch related checks and reuse compatible builds and caches. After fixing a failed check, rerun the affected checks when permitted. Broaden or repeat successful checks only when changed inputs, failures, or unresolved concerns justify it. +- Share machine resources during authorized heavy work by bounding agent-run parallelism or reducing process priority as needed. Commands supplied for the user run at normal priority with available parallelism; do not add low-priority wrappers or single-job limits to user-run commands. +- Test behavior and contracts rather than mirroring implementation details. Use mocks to check orchestration, and distinguish that coverage from executing the generated commands or consuming the resulting artifacts. +- Report what actually ran and what remains unverified. Static checks, mocked tests, and successful configuration do not establish native build, link, relocation, or runtime success. Distinguish confirmed failures, inferred causes, and fixes awaiting executable acceptance. + +### Local Windows tooling + +- On this workstation, reusable Windows tooling lives in `/mnt/projects/cheryl-windows-ci/`. Check it before downloading tools, creating another Wine environment, or reporting that Windows tooling is unavailable. If it is absent on another host, look for compatible installed tools there. +- The `tools/` directory contains Windows Python, CMake, Ninja, Git, ccache, and vcpkg. Read `env.sh` and `run.sh` for current paths, environment settings, and supported phases before using them; inspect the selected phase before execution. MSVC and the Windows SDK are installed under `/opt/msvc/`. +- Preserve `tools/`, `downloads/`, and `wine-prefix/` across sessions. Store additional reusable tools and isolated Wine prefixes under this disk-backed tooling root, preserving existing versions and active work. +- `/tmp` is memory-backed on this workstation. During RAM cleanup, preserve reusable tools and cached installers; move them to disk-backed storage when needed. Stop idle agent-owned processes and remove disposable test outputs within the authorized scope. +- These locations do not authorize builds, native tests, downloads, or changes to an active environment. The existing verification and shared-work restrictions still apply. + +## Report the result + +State what changed, which checks actually ran, and what remains unverified. +Include the tested revision and relevant configuration/selection when they affect +the conclusion. Separate an unexecuted case, a skipped case, a confirmed failure, +an inferred cause and a fix awaiting executable acceptance. + +For deferred requirements and queue maintenance, follow +[testing-request maintenance](testing-requests.md). Use the +[documentation workflow](documentation.md) for other authorized documentation +work. Follow [Git](git.md) for a coherent development-unit commit and its +verification limits. diff --git a/docs/for-agents/workflow.md b/docs/for-agents/workflow.md new file mode 100644 index 00000000..09e02793 --- /dev/null +++ b/docs/for-agents/workflow.md @@ -0,0 +1,114 @@ +# Shared work rules + +Read this guide for every task. The root [AGENTS.md](../../AGENTS.md) indexes +focused instructions; read the guides for the actual work being performed. +These instructions apply across the repository, including source comments outside +`docs/`. User authorization and constraints persist across relevant fixes and reruns. + +## Work modes and progression + +Each task has one primary work mode: + +- Review and planning +- Investigation and research +- Architecture and design +- Implementation, refactoring, and tooling +- Test design +- Test implementation +- Test execution +- Verification +- Documentation audit +- Documentation maintenance + +Keep independent work objectives separated by mode. A request for review or investigation does not authorize implementation merely because a fix becomes apparent. + +A request to implement or fix behavior includes necessary investigation, updates to affected tests, focused regression coverage where useful, and verification permitted by the [testing guide](testing.md). These supporting activities belong to the same development unit. Independent test design, broader verification campaigns, and documentation work remain separate objectives unless authorized. + +User-directed progression is the default: complete the requested objective and stop before starting an independent objective. When a bounded sequence would benefit from plan-directed progression, propose its phases and decision checkpoints before adopting it. An approved plan may authorize progression across modes without further permission for each coherent subtask. + +Stop when: + +- The requested scope is complete or an approved plan reaches a mandatory decision checkpoint. +- A material requirement, architectural decision, or invalidated assumption requires user direction. +- A necessary operation or scope expansion is not already authorized. +- Continuing would depend on unfinished user work or risk modifying protected changes. +- A blocking condition prevents safe or correct completion. + +A completed subtask or necessary supporting activity is not itself a stopping point. Continue authorized work that can safely proceed, including independent work unaffected by a blocked portion. Report any boundary, its implications, and the recommended next action. + +At handoff, distinguish implemented changes, observed verification, and outstanding or deferred work. Completing an implementation phase does not establish executable acceptance or complete a broader task with required work remaining. Optional improvements and advisory documentation checkpoints neither block completion nor authorize further work. + +## Source authority and context + +Prefer source code, configuration, and actual implementation when determining the repository's current state and behavior. + +Use documentation to understand intended architecture, requirements, design rationale, historical decisions, and planned work. Documentation may describe previous, intended, or partially implemented behavior. + +Verify documentation's claims about current implementation against the relevant code. When implementation and documented intent disagree, distinguish the observed state from the intended state rather than automatically treating either as correct. + +Acquire context purposefully. Inspect the code, documentation, history, and dependencies relevant to the current objective. Expand investigation when necessary rather than loading unrelated project context by default. + +Prefer concise phase handoffs containing established decisions, relevant constraints, outstanding work, and affected contracts. Do not create persistent handoff or planning documents unless explicitly requested. + +## Repository safety + +- Preserve active user runs and their outputs. Do not change their configuration, clean their build directories, or invalidate their caches as a routine preparation step. +- Do not perform opportunistic refactoring, cleanup, formatting, or API changes merely because they are convenient while working nearby. +- Report related work outside the current development unit as discovered work. Resolve necessary dependencies within the authorized scope; use the progression boundaries above when further work requires user direction. + +Git mutations and consolidation follow [Git](git.md). Builds, native execution, +full documentation generation, dependencies and devices follow the precise +permissions and resource rules in [Testing and verification](testing.md). +A mode guide or available tool does not grant authorization for its operations. + +## Shared working tree + +The repository is a shared human/agent working tree. Pre-existing uncommitted changes may represent deliberate work in progress and may be incomplete, temporarily inconsistent, non-functional, transitional, preparatory for later refactoring, or difficult to reason about safely. + +Before modifying affected code: + +- Inspect the current Git status and relevant pre-existing diffs. +- Treat pre-existing user changes as authoritative work in progress. Do not revert, overwrite, normalize, reformat, or replace them merely to simplify the task. +- Do not assume pre-existing changes form a valid, complete, or stable implementation baseline. +- Do not build new task work on top of pre-existing unfinished user work merely because its current behavior is understandable. +- Avoid introducing new dependencies on unfinished user work, propagating its current structure into additional code, constraining its future evolution, or otherwise increasing the amount of work that would need to change when the user's work is completed. + +A file containing user changes is not automatically off-limits. Work with it only when the requested change is independent of the unfinished work, or when the change can be made without increasing the dependency surface, constraining future evolution, or increasing the likely scope of subsequent refactoring. + +If completing the requested work would require treating unfinished user work as a stable foundation, extending its current design into other code, or making additional code depend on it, treat that as a development boundary. + +When such a boundary is encountered: + +- Preserve the existing user work unchanged where possible. +- Stop at that boundary. +- Continue with independent planned work only when it is clearly unaffected by, and does not depend on, the blocked work. +- Clearly identify what cannot be completed safely and why. + +The [Git guide](git.md#create-coherent-commits) owns separating task commits from user work. + +## Documentation phases + +Reading documentation is permitted in any work mode when relevant to understanding requirements, intent, constraints, or history. Prefer the relevant source code when establishing current behavior, and consult the relevant roadmap or subject document for direction rather than loading the entire planning folder. + +Documentation auditing and maintenance are separate work modes. Do not create, update, reorganize, or delete documentation during other work modes unless explicitly requested. + +Essential code comments and runtime logging directly required by the implementation are permitted. Do not perform general comment cleanup, logging improvements, or explanatory documentation work opportunistically. + +Defer documentation synchronization until a dedicated maintenance phase. Prefer stabilizing related implementation work before reconciling documentation. + +Recommend a documentation checkpoint at a meaningful phase boundary when architectural contracts stabilize, substantial implementation changes accumulate, or documentation is likely to have become materially inaccurate. + +Documentation checkpoints are advisory. Do not conduct a documentation audit or begin maintenance merely to make the recommendation. The user decides whether to continue engineering work or authorize documentation maintenance unless the transition was already approved. + +For authorized auditing and maintenance, use the +[documentation workflow](documentation.md), plus the +[API instructions](api-documentation.md) when declarations or comments are involved. +A documentation audit reports findings without changing documents. Maintenance +requires its own authorization or an already approved sequence. + +## Complete the requested scope + +Use the relevant mode guide's completion criteria. Report implemented changes, +observed verification and remaining decisions or acceptance separately. Finish +independent authorized work when another portion reaches a boundary; do not turn +an optional improvement into either a blocker or authorization for another task. diff --git a/docs/generation/static/appearance.js b/docs/generation/static/appearance.js deleted file mode 100644 index 5957c14d..00000000 --- a/docs/generation/static/appearance.js +++ /dev/null @@ -1,49 +0,0 @@ -/* Resolve the theme in the document head, before the page is painted. */ -(() => { - "use strict"; - - const storageKey = "cheryl-docs-theme"; - const systemTheme = window.matchMedia("(prefers-color-scheme: dark)"); - let preference = "system"; - - try { - const saved = window.localStorage.getItem(storageKey); - if (saved === "light" || saved === "dark") { - preference = saved; - } - } catch { - // Theme switching still works when browser storage is unavailable. - } - - function applyTheme() { - document.documentElement.dataset.theme = preference === "system" - ? (systemTheme.matches ? "dark" : "light") - : preference; - } - - applyTheme(); - systemTheme.addEventListener("change", applyTheme); - - document.addEventListener("DOMContentLoaded", () => { - const select = document.getElementById("cheryl-theme"); - if (!select) { - return; - } - - select.value = preference; - select.closest(".cheryl-appearance").hidden = false; - select.addEventListener("change", () => { - preference = select.value; - applyTheme(); - try { - if (preference === "system") { - window.localStorage.removeItem(storageKey); - } else { - window.localStorage.setItem(storageKey, preference); - } - } catch { - // Keep the current page usable without persistent storage. - } - }); - }); -})(); diff --git a/docs/generation/tests/test_repository_links.py b/docs/generation/tests/test_repository_links.py deleted file mode 100644 index 19f479bb..00000000 --- a/docs/generation/tests/test_repository_links.py +++ /dev/null @@ -1,201 +0,0 @@ -"""Owner-guide projection regressions without Sphinx or generated output.""" - -from __future__ import annotations - -import ast -import copy -from pathlib import Path -import re -import tempfile -import types -import unittest -from urllib.parse import unquote, urlsplit - - -EXTENSION = Path(__file__).resolve().parents[1] / "extensions/repository_links.py" -CHECKOUT = EXTENSION.parents[3] - - -class Element(dict): - def __init__(self, rawsource="", text="", *children, **attributes): - super().__init__(attributes) - self.children = list(children) - self.text = text - self.source = None - self.line = None - self.parent = None - for child in self.children: - child.parent = self - - def deepcopy(self): - return copy.deepcopy(self) - - def extend(self, children): - for child in children: - child.parent = self - self.children.append(child) - - def findall(self, kind): - for child in self.children: - if isinstance(child, kind): - yield child - yield from child.findall(kind) - - def replace_self(self, replacement): - index = self.parent.children.index(self) - self.parent.children[index] = replacement - replacement.parent = self.parent - - -class Reference(Element): - pass - - -class Pending(Element): - pass - - -class Download(Element): - pass - - -def make_reference(builder, source, document, fragment, content): - return Reference("", "", content, refuri=document + ".html" + ("#" + fragment if fragment else ""), - source_document=source, target_document=document, target_fragment=fragment, internal=True) - - -def production_functions(): - tree = ast.parse(EXTENSION.read_text(encoding="utf-8"), filename=str(EXTENSION)) - selected = [] - for statement in tree.body: - if isinstance(statement, ast.ImportFrom) and statement.module == "__future__": - selected.append(statement) - elif isinstance(statement, ast.Assign) and any( - isinstance(target, ast.Name) and target.id == "PROJECTED_GUIDES" for target in statement.targets - ): - selected.append(statement) - elif isinstance(statement, ast.FunctionDef) and statement.name in {"reference", "projected_reference", "rewrite_links"}: - selected.append(statement) - namespace = { - "Path": Path, - "unquote": unquote, - "urlsplit": urlsplit, - "make_refnode": make_reference, - "nodes": types.SimpleNamespace(Element=Element, document=Element, inline=Element, reference=Reference), - "addnodes": types.SimpleNamespace(download_reference=Download, pending_xref=Pending), - } - exec(compile(ast.Module(body=selected, type_ignores=[]), str(EXTENSION), "exec"), namespace) - return types.SimpleNamespace(**namespace) - - -LINKS = production_functions() - - -class ProjectionTests(unittest.TestCase): - def setUp(self): - temporary = tempfile.TemporaryDirectory(prefix="cheryl-guide-projection-") - self.addCleanup(temporary.cleanup) - self.root = Path(temporary.name) - self.docs = self.root / "docs" - self.docs.mkdir() - self.owner = self.root / "projects/modules/platform/native-glfw/README.md" - self.owner.parent.mkdir(parents=True) - self.owner.write_text("# Native GLFW\n", encoding="utf-8") - self.environment = types.SimpleNamespace( - found_docs={"modules/native-glfw"}, docname="engine-systems/input", - doc2path=lambda name: str(self.docs / (name + ".md")), - path2doc=self.document_name, - ) - self.app = types.SimpleNamespace( - srcdir=str(self.docs), env=self.environment, builder=object(), - config=types.SimpleNamespace(source_suffix={".md": "markdown"}), - _cheryl_repository_links=types.SimpleNamespace(root=self.root, source_url=self.source_url), - ) - - def document_name(self, path): - path = Path(path).resolve() - if path.is_relative_to(self.docs) and path.suffix == ".md": - return path.relative_to(self.docs).with_suffix("").as_posix() - return None - - def source_url(self, path, fragment="", query=""): - path = Path(path).resolve() - if not path.is_relative_to(self.root) or not path.exists(): - return None - result = "https://github.com/example/repo/blob/" + "a" * 40 + "/" + path.relative_to(self.root).as_posix() - return result + ("?" + query if query else "") + ("#" + fragment if fragment else "") - - def rewrite(self, node): - document = Element("", "", node) - LINKS.rewrite_links(self.app, document) - return document.children[0] - - def assert_projected(self, result, fragment="selection-and-dependency", source="engine-systems/input"): - self.assertEqual(result["target_document"], "modules/native-glfw") - self.assertEqual(result["target_fragment"], fragment) - self.assertEqual(result["source_document"], source) - self.assertTrue(result["internal"]) - - def test_wrapper_inventory(self): - wrappers = {} - for page in (CHECKOUT / "docs/modules").glob("*.md"): - included = re.search(r"```\{include\}\s+([^\n]+)", page.read_text(encoding="utf-8")) - if included: - owner = (page.parent / included[1]).resolve().relative_to(CHECKOUT).as_posix() - wrappers[owner] = "modules/" + page.stem - self.assertEqual(LINKS.PROJECTED_GUIDES, wrappers) - - def test_myst_link(self): - label = Element("", "Native window") - node = Pending("", "", label, reftype="myst", - reftarget="../../projects/modules/platform/native-glfw/README.md#selection-and-dependency") - result = self.rewrite(node) - self.assert_projected(result) - self.assertEqual(result.children[0].children[0].text, "Native window") - self.assertIsNot(result.children[0].children[0], label) - - def test_document_link(self): - result = self.rewrite(Pending("", "", reftype="myst", refdomain="doc", - reftarget="../projects/modules/platform/native-glfw/README", - reftargetid="selection-and-dependency", refdoc="modules/index")) - self.assert_projected(result, source="modules/index") - - def test_download_link(self): - result = self.rewrite(Download("", "", reftarget="../../projects/modules/platform/native-glfw/README.md#selection-and-dependency")) - self.assert_projected(result) - - def test_plain_link(self): - result = self.rewrite(Reference("", "", refuri="../../projects/modules/platform/native-glfw/README.md#selection-and-dependency")) - self.assert_projected(result) - - def test_unpublished_owner(self): - self.environment.found_docs.clear() - result = self.rewrite(Reference("", "", refuri="../../projects/modules/platform/native-glfw/README.md#selection-and-dependency")) - self.assertFalse(result["internal"]) - self.assertIn("/projects/modules/platform/native-glfw/README.md#selection-and-dependency", result["refuri"]) - self.assertIn("a" * 40, result["refuri"]) - - def test_source_link(self): - source = self.owner.parent / "include/window.h" - source.parent.mkdir() - source.write_text("#pragma once\n", encoding="utf-8") - result = self.rewrite(Reference("", "", refuri="../../projects/modules/platform/native-glfw/include/window.h?plain=1#L1")) - self.assertFalse(result["internal"]) - self.assertTrue(result["refuri"].endswith("/include/window.h?plain=1#L1")) - - def test_external_link(self): - node = Reference("", "", refuri="https://example.com/README.md#selection-and-dependency") - self.assertIs(self.rewrite(node), node) - - def test_existing_document(self): - self.environment.found_docs.add("assets/asset-loading") - node = Reference("", "", refuri="../assets/asset-loading.md#publication-and-retry") - self.assertIs(self.rewrite(node), node) - - def test_outside_checkout(self): - node = Reference("", "", refuri="../../../outside.md") - self.assertIs(self.rewrite(node), node) - - -if __name__ == "__main__": - unittest.main() diff --git a/docs/getting-started/create-a/enemy.md b/docs/getting-started/create-a/enemy.md deleted file mode 100644 index 88a162c9..00000000 --- a/docs/getting-started/create-a/enemy.md +++ /dev/null @@ -1,79 +0,0 @@ -# Create an Enemy - -Create an application-owned patrol class sharing the same sprite/material as a -[Player](player.md). Its behavior is independent of physical input and belongs -to simulation. This example adds visible autonomous movement without assuming -built-in AI, physics or an entity/world container. - -## Responsibilities and state - -Enemy retains immutable visual resources and owns a horizontal patrol interval, -position and phase. A triangular path reflects motion at the interval ends, -including when an update crosses several ends. Rendering reads the resulting -position without advancing behavior. - -Create `enemy.h`: - -```{literalinclude} ../../../projects/apps/tutorial-game/src/enemy.h -:language: cpp -``` - -Cell one gives this object a different appearance using the maintained two-cell -actor. If using another sprite, select a valid cell for its grid. The interval -describes the sprite pivot's world position; it does not account for collision -shape or image extent. Time is reduced before multiplying by speed so a large -finite elapsed value does not overflow distance accumulation. - -## Construct during initialization - -Include `enemy.h` and add `std::optional enemy_` to the game. -After loading/validating the actor and image material, keep a shared copy for -Enemy before moving a handle into Player: - -```cpp -auto sprite = CE::Assets::Sprites::Manager::get().get_asset("first-game", "actor"); -if (!sprite || !images_) - throw std::runtime_error("Player and Enemy require the actor and image material"); -player_.emplace(sprite, images_, glm::vec2(160.0f, 100.0f), left_, right_); -enemy_.emplace(std::move(sprite), images_, 80.0f, 500.0f, 220.0f); -``` - -Both instances retain the shared asset; neither copies the texture/geometry -implementation or depends on the other's position. The values fit First Game's -initial 640 by 360 framebuffer. A game with different bounds should choose its -own patrol interval. - -## Update and publish - -After the game's quit handling and camera observation: - -```cpp -player_->update(tick); -enemy_->update(tick.delta_seconds); -``` - -During frame preparation, begin the world pass and append both: - -```cpp -player_->append(pass); -enemy_->append(pass); -``` - -Authored order controls draw order subject to the selected pipeline. Enemy's -phase advances for every simulation update, even if no frame can be prepared. -Do not derive patrol progress from render count or mutate this object from a -native callback. - -## Release and observe - -Reset `enemy_` and `player_` during `deinit()` before dropping game materials. -Keep the existing binding cleanup and partial-initialization-safe optionals. -No separate shutdown method is needed for this CPU state/shared-handle class. - -The player should respond to input while the second cell patrols between the -chosen bounds. Changing one Enemy's interval/phase should not affect another. -Larger update intervals should still leave its pivot inside the interval. - -Add [an animation cursor](../how-to/animate-sprite.md) per instance when using an -animated definition. Behavioral states may use [State](../../engine-systems/state.md) -if useful; this class does not require a state machine or imply pathfinding. diff --git a/docs/getting-started/create-a/index.md b/docs/getting-started/create-a/index.md deleted file mode 100644 index ca1d7997..00000000 --- a/docs/getting-started/create-a/index.md +++ /dev/null @@ -1,28 +0,0 @@ -# Create a... - -These tutorials organize engine operations into application-owned classes. -Cheryl supplies input, resources and rendering; your game defines each class's -behavior, state and lifetime. Begin with the maintained -[First Game](../first-game.md), keeping its initialization/update/frame/cleanup -structure as the integration point. - -Player and Enemy include their headers from the -[maintained tutorial game](../../../projects/apps/tutorial-game/README.md). -The HUD guide provides a TGUI widget class that extends First Game; the -[interactive GUI guide](../how-to/create-gui.md) teaches RmlUi documents. -First Game owns the direct text rendering walkthrough. - -```{toctree} -:maxdepth: 1 - -Player -Enemy -HUD with TGUI -``` - -A Player responds to semantic actions, an Enemy owns autonomous behavior and a -HUD publishes game information. Share immutable assets between instances while -keeping positions, timers, animation cursors and score state independent. -Use the [How to...](../how-to/index.md) tasks when you need one operation in -greater focus; use [Engine Systems](../../engine-systems/index.md) for the -ownership and execution model. diff --git a/docs/getting-started/create-a/player.md b/docs/getting-started/create-a/player.md deleted file mode 100644 index 9a0c92f2..00000000 --- a/docs/getting-started/create-a/player.md +++ /dev/null @@ -1,77 +0,0 @@ -# Create a Player - -Create an application-owned class that moves a sprite from semantic input and -publishes its current appearance. Cheryl does not supply a built-in player, -entity system or collision model. Work in a copy of -[First Game](../first-game.md), keeping its startup, bindings, materials and -runtime hooks. This class replaces the example's small `Actor` value. - -## Responsibilities and state - -The game owns binding registration and loading because those operations require -the platform owner. Player retains immutable sprite/material handles and owns -position and movement policy. It receives action IDs rather than physical key -codes, so the game can change bindings without changing the class. - -Create `player.h`: - -```{literalinclude} ../../../projects/apps/tutorial-game/src/player.h -:language: cpp -``` - -Movement units are local pixels per simulation second. Both actions held gives -zero horizontal movement. This example selects cell zero and deliberately has -no collision or world-boundary behavior; add those policies to your game rather -than treating a renderer coordinate as an automatic physics constraint. - -## Construct after loading - -Include `player.h` and retain `std::optional player_` in the -game. Keep First Game's initialization of the camera, input bindings and -`images_` material. Replace its `actor_.emplace(...)` with: - -```cpp -player_.emplace(std::move(sprite), images_, glm::vec2(160.0f, 100.0f), left_, right_); -``` - -Here `sprite` is the validated `first-game:actor` handle loaded by the existing -initialization block, and `left_`/`right_` are the game's action IDs, not binding -registration IDs. The optional remains empty if construction fails. - -## Update and prepare - -Keep the game's quit handling and camera resize observation. Replace its Actor -movement/cell block with: - -```cpp -player_->update(tick); -``` - -Inside frame preparation, begin the world pass as before and replace the Actor's -image-style/`resolve_sprite()` block with: - -```cpp -player_->append(pass); -``` - -The existing score text may still be added afterward. Do not keep both image -paths unless you intend to draw both objects. Update changes authoritative -position; append only copies visible state and retains resources in packets. -Concurrent runtime can skip append calls without slowing movement. - -## Release and observe - -Replace `actor_.reset()` with `player_.reset()` in `deinit()` and keep the -existing binding removal and material/text cleanup. Player's shared handles are -released there; completed frame packets may still have their own retained -generations. Runtime recycles frames before backend shutdown. - -Left/right input should move one sprite while the quit action still stops the -game. Releasing movement stops it; holding both directions cancels movement. -Switching runtime execution mode should preserve the same ownership boundaries. -Input focus may intentionally gate keyboard gameplay while an editor is open. - -Use [binding actions](../how-to/bind-input.md), -[sprite animation](../how-to/animate-sprite.md) and -[camera placement](../how-to/use-camera.md) to extend this class. Animation adds -an independent cursor to Player; shared sprite metadata remains immutable. diff --git a/docs/getting-started/how-to/bind-input.md b/docs/getting-started/how-to/bind-input.md deleted file mode 100644 index 4fbe3f1f..00000000 --- a/docs/getting-started/how-to/bind-input.md +++ /dev/null @@ -1,97 +0,0 @@ -# Bind input actions - -Bind physical controls once during initialization, then let game updates read -semantic actions. This task uses the Native GLFW input implementation selected -by [First Game](../first-game.md). The result is a movement/quit mapping whose -game logic does not depend on upstream device-library constants. - -## Choose action and registration storage - -Add these game members, with `` and the input headers: - -```cpp -static constexpr CE::Input::Actions::Id quit_{1}; -static constexpr CE::Input::Actions::Id left_{2}; -static constexpr CE::Input::Actions::Id right_{3}; -std::array bindings_{}; -``` - -An action ID identifies game meaning. A binding ID identifies one registration -for later removal; they are not interchangeable. Keep the action IDs distinct -within the input system shared by your game. - -Find physical controls in the [control guide](../../runtime/input-state-model.md#control-identities-and-binding-tokens): -{cpp:enum}`CE::Input::KeyboardKey`, {cpp:enum}`CE::Input::MouseButton` and -{cpp:enum}`CE::Input::GamepadControl` are Engine enums accepted directly by -bindings. Pointer axes, scroll axes and wheel-button pulses live under -`CE::Input::MouseControl`. The guide lists their headers and how to pair each -family with its device ID. - -## Register on the platform owner - -Include `` and -``, and link the selected -[Native GLFW target](../../modules/native-glfw.md) with native input enabled. -During `init()`, after runtime initializes the input adapter: - -```cpp -auto& input = engine_.input(); -auto& bindings = input.bindings(); -const auto keyboard = input.keyboard_id(); -bindings_[0] = bindings.bind_button({keyboard, CE::Input::KeyboardKey::Escape}, quit_); -bindings_[1] = bindings.bind_button({keyboard, CE::Input::KeyboardKey::Left}, left_); -bindings_[2] = bindings.bind_button({keyboard, CE::Input::KeyboardKey::Right}, right_); -``` - -`CE::Input::Control` converts the enum when the binding is registered. The -adapter normalizes native observations to the same Engine control IDs. Obtain -device IDs from `input`; the standard bindings require no numeric casts or -adapter-specific translation. `Unknown` is not bindable, and a named gamepad -control is reported only when the selected backend/device supports it. - -## Read the tick during update - -```cpp -if (tick.input.button(quit_).pressed()) { - tick.request_stop(); - return; -} -const float direction = (tick.input.button(right_).held() ? 1.0f : 0.0f) - - (tick.input.button(left_).held() ? 1.0f : 0.0f); -position_.x += direction * 160.0f * static_cast(tick.delta_seconds); -``` - -`pressed()` reports an edge in the consumed observations; `held()` reports the -current semantic state. Runtime chooses the simulation delta independently of -poll count. For movement proportional to measured down-time within an interval, -use the documented `tick.button_simulation_seconds(action)` policy instead. - -Bindings remain platform-owned even in concurrent runtime mode. Do not install -or remove them from `update()`; submit an owned request to platform if live -rebinding is required. The tick view is borrowed for that hook. - -## Remove registrations during cleanup - -In `deinit()`, tolerate a partially completed initialization: - -```cpp -for (auto& binding : bindings_) { - if (binding != 0) { - static_cast(engine_.input().bindings().unbind(binding)); - binding = 0; - } -} -``` - -Only remove your own registrations. Runtime keeps input/window dependencies -alive through game cleanup, then detaches callbacks before destroying the window. - -Left/right should move the actor and Escape should request ordinary runtime -shutdown. With both directions held the example cancels movement. While an -exclusive keyboard-focused editor is active, keyboard gameplay actions are -intentionally gated; mouse/controller alternatives are a separate policy. - -Read [input contracts](../../runtime/input-state-model.md), -[capture and focus](../../runtime/input-state-model.md#focus-routing-and-editing-controls) -and [GUI routing](create-gui.md). Text input must use committed text records; -do not reconstruct characters from bound key actions. diff --git a/docs/getting-started/how-to/draw-sprite.md b/docs/getting-started/how-to/draw-sprite.md deleted file mode 100644 index e3bd82e0..00000000 --- a/docs/getting-started/how-to/draw-sprite.md +++ /dev/null @@ -1,87 +0,0 @@ -# Load and draw a sprite - -This task loads the maintained First Game actor and publishes it through a -camera pass. Complete [Project setup](../project-setup.md) and run -[First Game](../first-game.md) first so its assets, shaders and matching resource -provider are available. The result is one retained sprite at a chosen position. - -## Load on the platform owner - -Include the loader, sprite manager, shader manager, submission and camera -headers used in [the maintained source](../../../projects/apps/first-game/src/first-game.cpp). -During `init()`, load the asset root through the initialized provider: - -```cpp -CE::Assets::Loading::Loader loader(asset_root_); -loader.load_assets(engine_.resources()); -sprite_ = CE::Assets::Sprites::Manager::get().get_asset("first-game", "actor"); -images_ = CE::Assets::Shaders::Manager::get().find_material("first-game:images"); -if (!sprite_ || !images_) - throw std::runtime_error("The actor and image material are required"); -``` - -Store `sprite_` as `std::shared_ptr` and -`images_` as `std::shared_ptr`. The -[actor definition](../../../projects/apps/first-game/assets/graphics/definitions/sprites.json) -describes two cells; its image path is relative to the enclosing `graphics` -directory, independent of the definition file's nesting. The -[graphics index](../../../projects/apps/first-game/assets/graphics/graphics-manifests.json) -selects that definition and the material recipes. - -Loading can publish some entries before a later upload fails. Treat failure as -failed initialization and run partial-safe cleanup, following the -[loading contract](../../assets/asset-loading.md#publication-and-retry). - -## Keep visible state in the game - -Keep a `glm::vec2 position_{160.0f, 100.0f}`, a -`CE::Assets::Grids::CellIndex cell_ = 0` and `CE::Rendering::Camera2D camera_` -alongside those handles. During update: - -```cpp -camera_.set_framebuffer_size(tick.framebuffer_size); -``` - -Change position and cell there according to game state. Cells are zero-based -grid indices; choose one below `sprite_->definition().grid.cell_count()`. -Use the copied tick dimensions in concurrent mode instead of reading the window. - -## Resolve a packet during frame preparation - -In `prepare_render_frame(CE::Rendering::FrameWriter& frame) const`: - -```cpp -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::Submission::ImageParameter2D{"image", 0} -}; -CE::Rendering::DrawStyle2D style; -style.material = images_; -style.scale = 4.0f; -style.model_matrix = glm::translate(glm::mat4(1.0f), glm::vec3(position_, 0.0f)); -pass.add(CE::Rendering::Submission::resolve_sprite(*sprite_, cell_, style, context)); -``` - -Include `` for translation. The image parameter -key matches the material's public sampler key; it is not a native uniform name. -Style scale enlarges local geometry, while the model transform positions the -actor relative to its pivot. The camera uses framebuffer pixels and Y-up bounds. - -Resolution performs CPU work and retains the selected geometry/material/image -generation in the packet. Do not issue graphics calls or retain the borrowed -pass writer. Runtime handles publication and presentation. - -## Release and observe - -Reset the game's sprite/material handles during `deinit()`, after simulation has -stopped and while the backend remains alive. Frame packets keep their own copies -until recycling; a retained C++ handle cannot draw through a destroyed backend. - -The actor should appear at the chosen position and scale. If it does not, check -asset identity, selected cell, material sampler key and camera dimensions before -changing backend code. Continue with [animation](animate-sprite.md), -[camera placement](use-camera.md) or [a Player class](../create-a/player.md). -Detailed packet and lifetime rules remain in -[render submission](../../rendering/pipelines-and-materials.md) and -[residency](../../resources/resource-residency.md). diff --git a/docs/getting-started/how-to/index.md b/docs/getting-started/how-to/index.md deleted file mode 100644 index dab733db..00000000 --- a/docs/getting-started/how-to/index.md +++ /dev/null @@ -1,27 +0,0 @@ -# How to... - -Use these guides for one focused task after [Project setup](../project-setup.md) -and [First Game](../first-game.md). Each task identifies its loading, update, -frame-preparation and cleanup boundaries, then links the detailed contract. - -The [maintained tutorial game](../../../projects/apps/tutorial-game/README.md) -demonstrates input, sprites, animation, camera movement and direct text updates -with a TGUI controls panel. The interactive GUI task below teaches a separate -RmlUi document workflow; [Create a HUD](../create-a/hud.md) covers TGUI widgets. -Native observation of rendering, fonts and focus remains a separate acceptance -step from compiling an example. - -```{toctree} -:maxdepth: 1 - -Bind input actions -Load and draw a sprite -Play sprite animation -Display and update text -Position objects with a camera -Create an interactive GUI with RmlUi -``` - -For a complete application-owned class, continue with -[Create a...](../create-a/index.md). The system relationships behind these tasks -are introduced in [Engine Systems](../../engine-systems/index.md). diff --git a/docs/getting-started/how-to/update-text.md b/docs/getting-started/how-to/update-text.md deleted file mode 100644 index c89a91a8..00000000 --- a/docs/getting-started/how-to/update-text.md +++ /dev/null @@ -1,139 +0,0 @@ -# Display and update text - -Extend [First Game's text walkthrough](../first-game.md#put-text-on-screen) -with a score that changes during play. Text crosses three stages: CPU -layout/preparation, graphics upload, then CPU frame submission. This task uses -the existing `first-game:text` material and keeps an earlier frame's resources -valid while the next label uploads. - -## Retain the replacement state - -Keep First Game's `score_` generation and `text_` material. Add these members to -the game: - -```cpp -std::optional fonts_; -CE::Runtime::PlatformDispatcher::Submission text_platform_; -std::future pending_score_; -int requested_score_ = 0; -int displayed_score_ = 0; -int submitted_score_ = 0; -``` - -Include ``, ``, -``, ``, ``, ``, `` and `` -alongside First Game's text/submission headers. The optional font collection -allows loading during `init()` and cleanup after partial initialization. - -Game rules own the score's meaning. These three integers distinguish the latest -requested value, the value currently shown, and the value in the pending upload. - -## Load fonts and an initial generation - -Replace First Game's initial score block with this platform-owned `init()` code: - -```cpp -text_platform_ = engine_.platform_dispatcher().submission(); -CE::Text::FontSelection selection; -selection.preferred = {"DejaVu Sans"}; -selection.asset_directories = {asset_root_}; -selection.automatic_system_fonts = false; -fonts_.emplace(CE::Text::FontCollection::load(selection)); -auto layout = CE::Text::layout_text(*fonts_, "Score: 0", {.pixel_height = 24}); -auto prepared = CE::Text::Rendering::prepare(std::move(layout)); -score_.emplace(CE::Text::Rendering::upload(std::move(prepared), engine_.resources())); -``` - -Font selection snapshots bytes; layout/prepared values retain those snapshots. -Missing preferred fonts can fall back according to the collection contract. -Explicit application font assets give repeatable appearance across machines. -The saved dispatcher endpoint submits work without capturing a borrowed context. - -## Request a changed value - -Assign `requested_score_` when game rules change the score. For a simple -extension to First Game, add this after its existing gameplay updates so Space -also increments the score: - -```cpp -if (tick.input.button(next_cell_).pressed() && requested_score_ < std::numeric_limits::max()) - ++requested_score_; -``` - -Use an appropriate bounded score type/policy for the actual game. - -## Adopt and prepare without blocking simulation - -After handling score changes, run this block during each `update()`: - -```cpp -if (pending_score_.valid() && pending_score_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - auto replacement = pending_score_.get(); - score_.emplace(std::move(replacement)); - displayed_score_ = submitted_score_; -} -if (!pending_score_.valid() && requested_score_ != displayed_score_) { - auto prepared = CE::Text::Rendering::prepare( - CE::Text::layout_text(*fonts_, std::format("Score: {}", requested_score_), {.pixel_height = 24})); - pending_score_ = text_platform_.submit( - [prepared = std::move(prepared)](CE::Runtime::Context& owner) mutable { - return CE::Text::Rendering::upload(std::move(prepared), owner.resources()); - }); - submitted_score_ = requested_score_; -} -``` - -Keep drawing the previous complete generation while waiting. A ready exception -propagates before replacing `score_`; this example uses runtime's normal failure -cleanup. An application that catches it must choose its reporting/retry policy. -Do not call `get()` on a still-pending platform dependency from simulation. - -Only owned prepared data crosses to graphics. The callback captures neither the -game nor a borrowed string. Small-label layout/preparation runs on simulation; -larger content can use a tracked worker group with the same upload boundary. - -One pending request bounds queue growth. Further changes coalesce into the next -replacement, and unchanged values submit no work. For example, if `10` uploads -while the requested value advances through `11` to `12`, adoption displays `10` -and the next request prepares `12`. - -## Publish in screen coordinates and release - -Keep First Game's compatible text material and image key when submitting: - -```cpp -CE::Rendering::DrawStyle2D style; -style.material = text_; -style.model_matrix = - glm::translate(glm::mat4(1.0f), glm::vec3(24.0f, static_cast(camera_.framebuffer_size().height) - 48.0f, 0.0f)); -pass.add(CE::Rendering::Submission::resolve_text(*score_, style, context)); -``` - -Here `context` is the pass's `Context2D` with image parameter `"image"`, as in -[drawing a sprite](draw-sprite.md). The generation owns layout/atlas resources; -changing the source string alone cannot change an uploaded label. A new -generation leaves old frame packets valid. - -For a scrolling world, use -[First Game's separate screen pass](../first-game.md#keep-text-in-screen-coordinates) -instead of sharing the world camera. - -During `deinit()`, after runtime has settled accepted work and recycled frames: - -```cpp -score_.reset(); -fonts_.reset(); -``` - -Keep provider/material dependencies alive through the game's other cleanup. -The score should start at zero, update after replacement completes, remain -visible while waiting and preserve screen placement during resize and world -movement. Check both runtime modes; repeated unchanged updates should submit no -replacement uploads. - -This is the direct text rendering path. [Create a HUD with TGUI](../create-a/hud.md) -builds score and health widgets, while [the RmlUi GUI guide](create-gui.md) -covers documents and interactive editing. Read -[Unicode layout](../../assets/text-layout.md), -[encoding/source indices](../../assets/text-encoding.md) and -[resource publication](../../resources/resource-residency.md) for detailed limits. diff --git a/docs/getting-started/how-to/use-camera.md b/docs/getting-started/how-to/use-camera.md deleted file mode 100644 index 414b56d1..00000000 --- a/docs/getting-started/how-to/use-camera.md +++ /dev/null @@ -1,78 +0,0 @@ -# Position objects with a camera - -A camera supplies projection and view matrices; the game supplies object -transforms and chooses which camera each pass uses. Start from -[drawing a sprite](draw-sprite.md). This task separates moving an object in world -space from changing the view of that world. - -## Track drawable dimensions - -Keep a simulation-owned `CE::Rendering::Camera2D camera_`. During initialization -on platform, it may read `engine_.window().framebuffer_size()`. During update: - -```cpp -camera_.set_framebuffer_size(tick.framebuffer_size); -``` - -`Camera2D` projects Y-up framebuffer-pixel bounds from zero to width/height. -Logical window dimensions can differ on scaled displays. Negative pixel sizes -reject; zero sizes are retained while projection clamps each axis to at least -one. The camera does not turn logical input coordinates into pixels for you. - -## Move the object - -With ``, place the sprite through its model matrix: - -```cpp -style.model_matrix = glm::translate(glm::mat4(1.0f), glm::vec3(position_, 0.0f)); -``` - -The asset's pivot defines which local point reaches `position_`. Style scale -changes local geometry; it does not change the meaning of your world position. -Select your game's coordinate convention explicitly when extending beyond this -pixel-oriented example. - -## Change the view - -For a view whose world-space origin is `view_origin_`, update the camera on its -owner: - -```cpp -camera_.set_view_matrix(glm::translate(glm::mat4(1.0f), glm::vec3(-view_origin_, 0.0f))); -``` - -Moving the origin right shifts the displayed world left without changing any -entity's authoritative position. Keep `view_origin_` as application-owned -`glm::vec2` state. This is explicit positioning; a following-camera behavior -would choose that state according to its own game policy. - -## Copy matrices into passes - -During frame preparation: - -```cpp -auto world = frame.begin_pass(camera_.projection_matrix(), camera_.view_matrix()); -``` - -Populate `world` with resolved sprite/tile packets. A second HUD camera using an -identity view can begin a later pass so world scrolling does not move screen -information. Both writers borrow the current frame; retain neither them nor -camera matrix references beyond preparation. - -Camera revisions count changed values; they are not atomic publication. Runtime -transfers the copied matrices with the completed frame and renderer playback -does not read your live camera. - -## Observe the result - -Resizing should update visible pixel bounds. Changing `position_` moves just the -actor; changing `view_origin_` moves the world view. A separate HUD pass remains -anchored to its own projection. Keep all camera mutations in update and release -the game's resource handles during ordinary cleanup; cameras own CPU values and -need no backend shutdown call. - -Read [camera contracts](../../rendering/pipelines-and-materials.md#camera-observations), -[display dimensions](../../runtime/display-and-window-contract.md#sizes-modes-and-observation) -and [frame ownership](../../runtime/runtime-frame-boundary.md). Perspective -parameters belong to `Camera3D`; they do not automatically select depth policy -or a scene system. diff --git a/docs/guides/index.md b/docs/guides/index.md deleted file mode 100644 index 81820815..00000000 --- a/docs/guides/index.md +++ /dev/null @@ -1,23 +0,0 @@ -# Detailed Guides - -Choose the operation you need to perform. These guides connect working sequences -to their exact ownership, units, validation and failure contracts. For a first -sprite, text label or interface, start with the focused tasks in Getting Started. - -```{toctree} -:maxdepth: 2 - -Assets <../assets/index> -Text <../text/index> -Rendering and Resources <../rendering/index> -Input <../input/index> -GUI <../gui/index> -Audio <../audio/index> -Runtime and Concurrency <../runtime/index> -Memory, State and Utilities <../utilities/index> -Diagnostics <../diagnostics/index> -``` - -Use [Engine Systems](../engine-systems/index.md) to choose a system or public type, -and [Architecture](../architecture/index.md) when the task crosses component, -lifetime or thread boundaries. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 93596328..00000000 --- a/docs/index.md +++ /dev/null @@ -1,99 +0,0 @@ -# Cheryl Engine - -Build a game with Cheryl's application lifecycle, input actions, retained assets, -and rendering interfaces. Set up your project, then follow the first-game -walkthrough from startup and resource ownership to sprites and text on screen. -Use the task guides and generated reference as you add features. - -```{toctree} -:maxdepth: 1 -:caption: API Reference - -C++ API -``` - -```{toctree} -:maxdepth: 2 -:caption: Getting Started - -Project setup -First game -Create a... -How to... -``` - -```{toctree} -:maxdepth: 1 -:caption: Engine Systems - -Overview -Runtime -Display -Input -Assets -Resources -Rendering -Text -GUI -Audio -Events -Dispatch -Workers -Memory -State -Logging -Diagnostics -Utilities -``` - -```{toctree} -:maxdepth: 3 -:caption: Detailed Guides - -Choose a guide -``` - -```{toctree} -:maxdepth: 1 -:caption: Modules - -Selection and roles -Source and SDK consumption -Startup -Workers -Native GLFW -OpenGL -TGUI -RmlUi -miniaudio -Linux Debug terminal -``` - -```{toctree} -:maxdepth: 1 -:caption: Architecture - -Overview -Interfaces and Adapters -Assembling the Engine -Ownership and Cleanup -Threads and Data Flow -``` - -```{toctree} -:maxdepth: 1 -:caption: Development - -Contribute a change -Build and produce SDKs -Write a module -Write a UI adapter -Follow C++ style -Select tests -Validate architecture -Validate logging -Check native desktop behavior -Generate documentation -Publish documentation -Operate CI and SDK collection -``` diff --git a/docs/modules/debug-terminal-linux.md b/docs/modules/debug-terminal-linux.md deleted file mode 100644 index 6c243ec0..00000000 --- a/docs/modules/debug-terminal-linux.md +++ /dev/null @@ -1,6 +0,0 @@ -```{include} ../../projects/modules/platform/debug-terminal-linux/README.md ---- -relative-docs: . -relative-images: true ---- -``` diff --git a/docs/modules/index.md b/docs/modules/index.md deleted file mode 100644 index 4b41089c..00000000 --- a/docs/modules/index.md +++ /dev/null @@ -1,57 +0,0 @@ -# Modules - -Choose modules for the capabilities your application needs, then link their -public CMake targets. Engine supplies portable contracts; concrete modules -implement related responsibilities without becoming dependencies of neutral -Engine consumers. A system name in [Engine Systems](../engine-systems/index.md) -does not imply a separate library. - -For the interfaces a replacement must fulfill, use the -[adapter contract catalogue](../architecture/interfaces-adapters.md). -It separates runtime requirements from optional backend capabilities and -application-owned integrations. - -| Capability | Owner guide | What it adds | -| --- | --- | --- | -| Application bootstrap | [Startup](startup.md) | Command-line configuration, configured module integration and standard context creation. | -| Worker configuration/topology | [Workers](workers.md) | Optional JSON settings and native topology over Engine-owned pools. | -| Native display and input | [Native GLFW](native-glfw.md) | Window/monitor operations, input collection and native lifetime management. | -| Graphics and presentation | [OpenGL](opengl.md) | Rendering, graphics resources and presentation using Native GLFW. | -| Native widgets | [TGUI](tgui.md) | TGUI widgets, routed input and retained scene publication. | -| Native documents | [RmlUi](rmlui.md) | RML/RCSS documents, routed input and retained scene publication. | -| Audio | [miniaudio](miniaudio.md) | Decoding, device/offline output and streamed playback without window/graphics requirements. | -| Process output viewer | [Linux Debug terminal](debug-terminal-linux.md) | Optional Linux process attachment and output-viewer lifetime. | - -These pages publish the canonical guides kept beside each owner's source. -Selection flags, public targets, dependency requirements, standalone composition -and limits are documented with the owner. The -[repository module index](../../projects/modules/README.md) also maps those owners -to the portable contracts they implement. - -## Select an assembly - -[Assembling the Engine](../architecture/assembling-engine.md) connects the build -facade, public includes and startup factories. Use -[source and SDK consumption](../development/consuming-engine.md) for complete -application CMake and deployment instructions. - -Start with [Project setup](../getting-started/project-setup.md) for the supported -source-checkout and installed-SDK paths. Availability in the configured build -and linkage into your application are different choices: an enabled module -becomes usable through its selected public target. Link the published target to -inherit its required usage settings; avoid assembling private support targets -or repeating their implementation libraries in your application. - -Native GLFW and OpenGL form a compatible native graphics path. Audio can be -used independently. TGUI and RmlUi are independent integrations; their -application-owned sessions use Engine input/rendering contracts instead of -requiring Native GLFW or OpenGL in their public APIs. Platform-specific process -attachments have different lifetime and deployment needs from ordinary modules. - -## Learn or extend - -[GUI construction](../gui/index.md) explains application-facing widget/document -integration. [Source and SDK consumption](../development/consuming-engine.md) -covers linking and startup. [Module authoring](../development/modules.md) and -[UI adapter authoring](../development/ui-adapters.md) describe extension rules; -they remain in Development rather than being copied into every owner guide. diff --git a/docs/modules/miniaudio.md b/docs/modules/miniaudio.md deleted file mode 100644 index c8bdd151..00000000 --- a/docs/modules/miniaudio.md +++ /dev/null @@ -1,6 +0,0 @@ -```{include} ../../projects/modules/audio/miniaudio/README.md ---- -relative-docs: . -relative-images: true ---- -``` diff --git a/docs/modules/native-glfw.md b/docs/modules/native-glfw.md deleted file mode 100644 index 2fa5f799..00000000 --- a/docs/modules/native-glfw.md +++ /dev/null @@ -1,6 +0,0 @@ -```{include} ../../projects/modules/platform/native-glfw/README.md ---- -relative-docs: . -relative-images: true ---- -``` diff --git a/docs/modules/opengl.md b/docs/modules/opengl.md deleted file mode 100644 index 948eb4d2..00000000 --- a/docs/modules/opengl.md +++ /dev/null @@ -1,6 +0,0 @@ -```{include} ../../projects/modules/graphics/opengl/README.md ---- -relative-docs: . -relative-images: true ---- -``` diff --git a/docs/modules/rmlui.md b/docs/modules/rmlui.md deleted file mode 100644 index e051f44c..00000000 --- a/docs/modules/rmlui.md +++ /dev/null @@ -1,6 +0,0 @@ -```{include} ../../projects/modules/ui/rmlui/README.md ---- -relative-docs: . -relative-images: true ---- -``` diff --git a/docs/modules/startup.md b/docs/modules/startup.md deleted file mode 100644 index 0b01532c..00000000 --- a/docs/modules/startup.md +++ /dev/null @@ -1,6 +0,0 @@ -```{include} ../../projects/modules/runtime/startup/README.md ---- -relative-docs: . -relative-images: true ---- -``` diff --git a/docs/modules/tgui.md b/docs/modules/tgui.md deleted file mode 100644 index 98862ca4..00000000 --- a/docs/modules/tgui.md +++ /dev/null @@ -1,6 +0,0 @@ -```{include} ../../projects/modules/ui/tgui/README.md ---- -relative-docs: . -relative-images: true ---- -``` diff --git a/docs/modules/workers.md b/docs/modules/workers.md deleted file mode 100644 index 495a3d34..00000000 --- a/docs/modules/workers.md +++ /dev/null @@ -1,6 +0,0 @@ -```{include} ../../projects/modules/runtime/workers/README.md ---- -relative-docs: . -relative-images: true ---- -``` diff --git a/docs/planning/README.md b/docs/planning/README.md index 9d6310cc..7c63ae3c 100644 --- a/docs/planning/README.md +++ b/docs/planning/README.md @@ -2,19 +2,38 @@ Horizons describe sequencing, not delivery dates. Detailed checklists live in the plan owning each active task; current contracts live in the [subject guides](../README.md). -The [status inventory](../STATUS.md) records implementation classifications. -The [roadmap](develop-review-and-development-plan.md#nearest-planned-work) owns the -nearest work and acceptance checkpoints, -and [testing requests](../testing-requests.md) contains pending user-run acceptance. - -| Folder | Scope and owning plans | -| --- | --- | -| `short-term/` | [Unicode font-selection regressions](short-term/unicode-text.md). The roadmap owns [startup/UI follow-up](develop-review-and-development-plan.md#startup-and-ui-follow-up) and links pending main-push SDK collection, terminal desktop QA and demo acceptance to the testing queue. Completed cache-locality source work lives in the worker guide. | -| `mid-term/` | [Planned color emoji and reorder-safe sorting, with deferred general batching and measured text/timing optimization](mid-term/README.md); design and workload prerequisites remain | -| `long-term/` | [Planned RmlUi effects, alternative audio and the graphical console alongside deferred integrations, NUMA and consumer-selected extensions](long-term/README.md), [deferred HID/platform acceptance](long-term/platform-acceptance.md) and [unresolved artwork metadata](long-term/asset-manifest-metadata.md) | - -The roadmap owns the three selected -[GoogleTest, nlohmann/json and spdlog dependency reviews](develop-review-and-development-plan.md#dependency-reviews). +The [status inventory](../STATUS.md) owns implementation classifications, selected +work and deferred review order. [Testing requests](../testing-requests.md) owns +ready pending user-run acceptance; the plans below retain unfinished work and +its prerequisites. + +## Find the owning plan + +Use this table to reach the actual requirements. Selection and deferred phase order +remain in STATUS; a horizon is not a delivery commitment. + +| Horizon | Plan or section | Next checkpoint | +| --- | --- | --- | +| Short term | [Font-selection regressions](short-term/README.md#font-selection-regressions) | Cover automatic weight boundaries and explicit preferences. | +| Short term | [Startup and UI follow-up](short-term/README.md#startup-and-ui-follow-up) | Cover successful context/runtime handoff, callback/batch and placeholder cases; obtain a corrected supplied RmlUi host. | +| Short term | [Upstream placeholder follow-up](short-term/README.md#upstream-placeholder-follow-up) | Reproduce against unmodified upstream code before proposing a regression or submission. | +| Selected review | [Dependency reviews](#dependency-reviews) | Assess GoogleTest, nlohmann/json and spdlog independently; recommend exact pins. | +| Mid term | [Color emoji](mid-term/README.md#color-emoji) | Select formats and sequences; settle RGBA pages, materials and fallback. | +| Mid term | [Reorder-safe sorting](mid-term/README.md#reorder-safe-render-sorting) | Identify reorderable regions and a representative workload. | +| Mid term | [General batching](mid-term/README.md#general-render-batching) and [measured optimization](mid-term/README.md#measured-optimization) | Gather measurements before selecting an implementation. | +| Long term | [UI extensions](long-term/README.md#ui-adapters) | Settle RmlUi effects and required rendering/backend contracts. | +| Long term | [Alternative audio](long-term/README.md#alternative-audio-sdk) and [graphical console](long-term/README.md#graphical-developer-console) | Settle SDK/deployment or command/history/input requirements. | +| Long term | [Native integration](long-term/README.md#deferred-native-integration) and [platform acceptance](long-term/platform-acceptance.md) | Select a consumer/platform and supply the backend or observation prerequisites. | +| Long term | [Steam services and input](long-term/README.md#steam-api-and-input-integration) | Establish SDK-free input/session policy before selecting transport. | +| Long term | [Residency/cache domains](long-term/README.md#residency-accounting) and [monitor hotplug](long-term/README.md#monitor-hotplug) | Settle accounting or inventory identity/lifetime before adding policy. | +| Long term | [Atlas padding](long-term/README.md#atlas-padding-and-repacking) and [NUMA locality](long-term/README.md#numa-locality) | Select scope for padding; NUMA remains indefinitely deferred. | +| Long term | [Artwork metadata](long-term/asset-manifest-metadata.md) | Resolve source metadata and consumer-specific manifest choices. | +| Long term | [Additional dungeon manifests](long-term/README.md#additional-dungeon-asset-manifests) | Inventory acquired packages and author each manifest independently. | + +For implemented work awaiting execution, use [STATUS's acceptance summary](../STATUS.md#remaining-acceptance) +and the [testing queue](../testing-requests.md). Those procedures are not development +plans. + Deferred phases in the status inventory describe review order within that section; they do not replace these subject horizons or authorize implementation. @@ -24,3 +43,36 @@ The current RmlUi core integration is sufficient for the selected Linux scope; [rendering effects](long-term/README.md#ui-adapters) are planned, including any required backend support. OS services and additional toolkit selection remain deferred. + +## Dependency reviews + +The three reviews selected in [STATUS](../STATUS.md#2-planned) concern GoogleTest, +nlohmann/json and spdlog. Keep them independent. + +For each dependency, compare its pinned commit with upstream history and candidate +revisions, including changed ancestry and tag/commit availability. Review release +notes, integration/API changes, CMake targets/options, supported compilers and +license changes. Check Cheryl-owned source and supplied target/package paths where +applicable. Recommend an exact reproducible candidate or retention of the current +pin; a review does not itself select an update. + +If an update is selected, keep each submodule change a separate development unit. +Design focused acceptance for affected owners, independent consumers and the +Linux/Windows CI configurations. Preserve unrelated pins and local dependency work. + +## Topics awaiting plans + +The [deferred STATUS entries](../STATUS.md#3-deferred) include topics with no +concrete plan yet: expanded manifest formats and automatic asset hot reload. +ECS, tilemap/world, physics, save serialization, 3D and networking currently have +only a [consumer-required deferral](long-term/README.md#game-systems). Spatial +audio and OS/UI services likewise need specific design and acceptance checkpoints +beyond their [audio](long-term/README.md#alternative-audio-sdk) and +[UI](long-term/README.md#ui-adapters) scope notes. + +When one is selected, establish its consumer, scope, contracts, prerequisites and +observable acceptance in an owning plan, then link that plan from this index and +STATUS. Conditional possibilities mentioned in a plan remain unselected until +promoted explicitly. When an actual plan completes, preserve its resulting +contracts and unresolved acceptance, remove the completed plan and reconcile both +indexes. diff --git a/docs/planning/develop-review-and-development-plan.md b/docs/planning/develop-review-and-development-plan.md deleted file mode 100644 index 2eda0e5a..00000000 --- a/docs/planning/develop-review-and-development-plan.md +++ /dev/null @@ -1,157 +0,0 @@ -# Development roadmap - -The [planning catalogue](README.md) groups work by horizon. Linux/GLFW/X11/OpenGL -is the active native platform; deferred platform coverage does not gate Linux work. -Current architecture and API contracts live in the [subject guides](../README.md). - -## Nearest planned work - -This roadmap owns the nearest selected work; the catalogue and status inventory -link here. Priorities do not authorize implementation, builds or mode transitions. -The selected cache-locality → positioned-text → CI/SDK source work is complete. -Its contracts live in the [worker guide](../runtime/worker-execution.md), -[text guide](../assets/text-layout.md#positioned-text-runs) and -[build/CI guide](../development/ci.md). - -### Selected planned work - -The following work is selected for investigation and design. Dependency reviews -remain three independent tasks; each feature needs its own settled scope and -acceptance before implementation. This selection does not promote general packet -batching or the other deferred integrations. - -| Work | Owning scope and next checkpoint | -| --- | --- | -| GoogleTest submodule review | [Dependency review](#dependency-reviews): compare upstream history with the current pin, assess test/CMake/toolchain changes and recommend retaining the pin or selecting an update. | -| nlohmann/json submodule review | [Dependency review](#dependency-reviews): assess parsing, serialization, configuration and source/package compatibility against upstream candidates. | -| spdlog submodule review | [Dependency review](#dependency-reviews): assess logger/sink behavior, formatting dependencies, exported targets and source/package/toolchain compatibility. | -| RmlUi rendering effects | [UI extension scope](long-term/README.md#ui-adapters): settle masks, transforms, layers, filters, custom shaders and repeating coordinates, including required neutral-contract and graphics-backend support. | -| Reorder-safe render sorting | [Sorting scope](mid-term/README.md#reorder-safe-render-sorting): identify reorderable regions and measure a policy that preserves authored order where required, clipping and retained generations. | -| Color emoji glyphs | [Emoji scope](mid-term/README.md#color-emoji): select color-font formats and sequences, then settle RGBA pages, materials, fallback and CPU/native acceptance. | -| Alternative audio SDK | [Audio scope](long-term/README.md#other-engine-extensions): select an SDK, assess licensing/deployment and preserve the ordinary playback contract; any richer authoring model needs explicit scope. | -| In-game developer console | [Console scope](long-term/README.md#other-engine-extensions): settle graphical output, commands, scrolling/history and input routing independently of the native terminal. | - -The current RmlUi document/font/input/clipping and retained-upload integration -remains sufficient for the selected Linux root composition. Its planned rendering -effects include any required backend work; the -[startup/UI follow-up](#startup-and-ui-follow-up) keeps regression and supplied-Core -acceptance separate from extending that integration. - -### Dependency reviews - -For each dependency, compare its pinned commit with upstream history and candidate -revisions, including changed ancestry or tag/commit availability. Review relevant -release notes, integration/API changes, CMake targets/options, supported compilers -and license changes. Check both Cheryl-owned source and supplied target/package -paths where applicable. Recommend an exact reproducible candidate or retention of -the existing pin; reviewing history does not automatically authorize an update. - -If an update is selected, keep each submodule change a separate development unit -and design focused acceptance for the affected owners, independent consumers and -Linux/Windows CI configurations. Preserve unrelated pins and local dependency work. - -### Remaining acceptance - -Remaining acceptance has independent prerequisites: - -| Work | Next phase and checkpoint | -| --- | --- | -| Main-push SDK collection | Hosted Ubuntu/Windows builds and relocated SDK consumers are accepted. Verify the first successful main-push collection gate in [TR20](../testing-requests.md#tr20-automated-main-push-sdk-collection). PR/develop/manual runs intentionally skip collection. | -| Hosted Windows skipped cases | Supply a CFF font fixture and a temporary directory on the checkout drive, then accept the affected font-allocation and logging cases on hosted Windows. The [CI guide](../development/ci.md#hosted-windows-coverage-limits) retains their scope and prerequisites. | -| Linux Debug terminal | Complete real-desktop [TR18](../testing-requests.md#tr18-qa-linux-debug-terminal-on-a-real-desktop) using matching binaries from the accepted configuration matrix. Observe actual emulator windows and desktop/IDE application launch. | -| Demo material/artwork QA | Complete shader-pair failure/recovery and available artwork observations in [TR19](../testing-requests.md#tr19-qa-indexed-demo-materials-and-recovered-graphics). Missing images/showcase fixtures remain explicit blockers. | - -Sampling/TGUI and indexed shader/material loading have selected Linux CPU/backend -and native pixel acceptance. Main text/image materials use indexed recipes; -UI adapters use [provider-owned defaults](../rendering/pipelines-and-materials.md#default-ui-materials) -and accept optional custom material handles. -The reusable [graphics procedure](../development/architecture-validation.md#sampling-and-positioned-text) -preserves coverage limits. Completed implementation plans are retired after their -contracts and unresolved acceptance have moved to the subject guides/testing queue. - -NUMA discovery and memory placement remain [indefinitely deferred](long-term/README.md#other-engine-extensions). -General packet batching remains deferred under the -[mid-term scope](mid-term/README.md#general-render-batching). Reorder-safe sorting -is selected separately; positioned text runs do not implement either policy. -[Release publication](long-term/README.md#other-engine-extensions) -remains a future decision: current SDK outputs are workflow artifacts and contain -prebuilt development libraries/headers, not applications or a demo package. - -## Work-mode progression - -Finish each workstream's authorized phase before advancing: investigation and -research, architecture and design, test design, implementation/refactoring/tooling, -test implementation, test execution, then verification. Complete and commit coherent -implementation units independently. Keep documentation maintenance separate and -recommend synchronization when contracts stabilize; testing-request maintenance -also requires authorization. - -User-directed progression remains the default under [AGENTS.md](../../AGENTS.md). -A bounded plan-directed sequence may be approved, with mandatory checkpoints for -unresolved design decisions, changed scope or protected unfinished user work. -Builds and test execution still require explicit authorization. Source completion -and executable/native acceptance remain distinct. - -## Existing regression and composition follow-up - -The remaining [Unicode font-selection regressions](short-term/unicode-text.md) -and startup/UI checklist below are independent testing/composition objectives. -They are not blanket pending QA for the accepted Linux baseline. Select them in -their own work modes rather than mixing them into integration implementation. -The [testing queue](../testing-requests.md) owns ready pending acceptance; -the [demo guide](../../projects/apps/demo/README.md#repeating-native-qa) preserves -reusable native procedures and coverage limits. Existing regressions do not close -the dedicated-case gaps listed in the plans. - -### Startup and UI follow-up - -The [application guide](../development/consuming-engine.md#command-line-startup) -owns startup/result lifetimes and option semantics; the UI guides own -[batch routing](../development/ui-adapters.md#routing-and-unavailable-services) and -[RmlUi correction declarations](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract). -Font-weight acceptance remains with the Unicode plan rather than this checklist. - -- [x] Add common/backend startup support and migrate the demo's application options. -- [x] Preserve session entry epochs across complete input batches with per-record controls. -- [x] Correct RmlUi field focus, placeholder markup and vertical list rows; apply the - downstream source guard and require correction declarations for supplied Core. -- [x] Compile Engine-only/native UI selections and accept existing font/layout/resource - and session/coexistence checks with Cheryl-owned dependencies. -- [x] Compile independent Engine/OpenGL Startup first-include probes through the - installed SDK consumer assembly. -- [ ] Add dedicated Startup parser/factory, callback/batch routing and placeholder - hit-testing regressions in separately authorized - testing phases. Existing suites do not exercise these new paths directly. -- [x] Accept Linux native startup/field/focus/list observations in the selected - root composition; [demo QA](../../projects/apps/demo/README.md#startup-and-ui-validation) - retains the procedure and coverage limits. -- [ ] Obtain a corrected supplied RmlUi target/package and consuming host, then - accept declaration/rejection behavior independently of Cheryl-owned source. - -The [upstream investigation](../external-work/rmlui-placeholder-issue.md) retains -the unmodified reproduction, sanitizer and submission prerequisites. Downstream -demo success does not close that work. Dynamic backend selection remains outside -the current programmatic startup contract and has no selected implementation phase. - -## Deferred integrations - -Multiple active rendering windows and HID integration belong to the -[long-term native plan](long-term/README.md#deferred-native-integration). -Toolkit OS services remain [deferred UI extensions](long-term/README.md#ui-adapters). - -### Deferred HID integration - -The [long-term HID section](long-term/README.md#deferred-hid-integration) owns the -backend deferral, Gainput handoff and observation prerequisites. -[TR6](long-term/platform-acceptance.md#deferred-hid-lifecycle-and-windows-notifications) -is deferred there, outside the active testing queue. - -## Later work - -Use the [horizon catalogue](README.md) for work beyond the nearest sequence. -Color emoji and reorder-safe sorting are selected above, with their design and -workload prerequisites retained. General batching and text/timing optimization -remain deferred. RmlUi effects, alternative audio and the graphical console are -also selected; platform, Steam and other optional modules retain their existing -consumer and prerequisite requirements. Deferred phases in the -[status inventory](../STATUS.md#3-deferred) describe review order within that section. diff --git a/docs/planning/long-term/README.md b/docs/planning/long-term/README.md index 8eec3050..e8ef917d 100644 --- a/docs/planning/long-term/README.md +++ b/docs/planning/long-term/README.md @@ -3,7 +3,7 @@ This guide retains the scope and prerequisites for deferred integrations and consumer-driven extensions. RmlUi rendering effects, an alternative audio SDK and the in-game developer console are now selected in the -[planned roadmap](../develop-review-and-development-plan.md#selected-planned-work). +[status inventory](../../STATUS.md#2-planned). Other unselected engine and module extensions still need a named application consumer to establish scope and prerequisites. Additional asset manifests below remain deferred to the long term. Steam API services, Steam Input and their @@ -12,11 +12,11 @@ Multi-platform testing is also shelved; Linux/X11 remains the active short-term platform. Use the current ownership, lifetime, selection and standalone contracts in the -[module guide](../../development/modules.md). Its -[module boundary criteria](../../development/modules.md#module-boundary-criteria) +[module guide](../../site/development/modules.md). Its +[module boundary criteria](../../site/development/modules.md#module-boundary-criteria) govern new owners. Reusable isolation/composition procedures and native/fixture coverage limits are in -[architecture validation](../../development/architecture-validation.md). +[architecture validation](../../site/development/architecture-validation.md). ## Multi-platform acceptance @@ -40,7 +40,7 @@ are selection prerequisites; no mobile or console module is implemented. ### Multiple active rendering windows Multiple active rendering windows and switching the selected rendering window are -deferred. The [display/window contract](../../runtime/display-and-window-contract.md) +deferred. The [display/window contract](../../site/runtime/display-and-window-contract.md) allows a display to own several windows but permits one active rendering selection; replacement of that selection rejects. Window ownership alone does not establish multiple graphics contexts, presentation surfaces or independent resource domains. @@ -57,7 +57,7 @@ Windows worker CPU affinity is deferred to phase 4 of the [status inventory](../../STATUS.md#3-deferred), alongside platform acceptance and OS services. The current Windows hwloc provider retains CPU/core/cache discovery but reports thread binding unavailable because its backend rejects -`HWLOC_CPUBIND_NOMEMBIND`. The [worker contract](../../runtime/worker-execution.md#cpu-policy-and-capabilities) +`HWLOC_CPUBIND_NOMEMBIND`. The [worker contract](../../site/runtime/worker-execution.md#cpu-policy-and-capabilities) requires CPU binding to preserve memory placement; preferred policies use portable scheduling and required CPU/cache locality requests reject. @@ -92,7 +92,7 @@ so explicit startup-failure reporting requires a contract change if selected. Broader native ownership requires a consumer case. Battery and touch/motion exposure remain separate from the handoff's initial scope; use the -[native capability limits](../../../projects/modules/platform/native-glfw/README.md#hid-capability-and-platform-scope) +[native capability limits](../../site/modules/native-glfw.md#hid-capability-and-platform-scope) when selecting extensions. Windows/macOS source, fallback and notification work remains with the deferred platform scope. @@ -144,7 +144,7 @@ a concrete consumer requirement. Coordinate process-wide Gainput HID collection, shutdown and notification ownership before supporting them; independent `Init`/`Exit` calls cannot establish that behavior. Preserve one collection path per effective controller. The current -[native adapter lifetime](../../../projects/modules/platform/native-glfw/README.md#input-lifetime-and-mapping) +[native adapter lifetime](../../site/modules/native-glfw.md#input-lifetime-and-mapping) permits one initialized owner and retains its original Windows notification window. Explicit HID startup-failure reporting follows the [deferred HID contract](#deferred-hid-integration); ownership checks do not supply @@ -158,12 +158,12 @@ The RmlUi core integration supplies native documents/fonts, keyboard/text/pointe input, rectangular clipping, retained CPU/GPU scenes and lifecycle handling. Its selected Linux root-composition baseline remains usable while rendering effects are planned. Existing dedicated regressions and supplied-Core acceptance remain in -the [startup/UI follow-up](../develop-review-and-development-plan.md#startup-and-ui-follow-up). +the [startup/UI follow-up](../short-term/README.md#startup-and-ui-follow-up). RmlUi masks, nonidentity transforms, offscreen layers, filters, custom shaders and repeating texture coordinates are planned extensions and currently reject publication under the -[current recording contract](../../../projects/modules/ui/rmlui/README.md#recording-and-publication). +[current recording contract](../../site/modules/rmlui.md#recording-and-publication). Determine which effects can be implemented by the adapter and which require changes to neutral render/resource contracts and the graphics backend. Include necessary OpenGL/backend support in the selected task, covering render targets, @@ -181,7 +181,7 @@ authoring API. Similarity to existing bridges does not require shared widgets. Each toolkit remains independently selectable and depends on neutral Engine contracts; toolkit code must not become an Engine dependency. TGUI and RmlUi establish the current boundary in the -[adapter-author guide](../../development/ui-adapters.md). +[adapter-author guide](../../site/development/ui-adapters.md). ### Additional graphics backends @@ -193,24 +193,28 @@ omit/replace/test benefit; a seam alone does not justify it. ## Other engine extensions +### Graphical developer console + The built-in graphical developer console is planned as the engine's own graphical interface. Its scope consists of that console, with the renderer supplying its rendering; other application UI remains consumer-owned. It supplies eventual in-app console output independently of whether an application selects the optional native terminal. Establish scrolling/history, input routing and command requirements before -implementation. Until then, Release uses its existing logging destinations. The -implemented [Linux Debug terminal](../../../projects/modules/platform/debug-terminal-linux/README.md) uses a native terminal and can -provide diagnostics independently of game rendering; closing it leaves the -application running. +implementation. Keep it independent of the optional +[native terminal](../../site/modules/debug-terminal-linux.md). + +### Text coverage -Beyond the initial [grayscale scope](../../assets/text-layout.md#native-acceptance-scope), +Beyond the initial [grayscale scope](../../site/assets/text-layout.md#native-acceptance-scope), consider wider CJK/script acceptance when a consumer establishes its required fonts, language-specific shaping, line-breaking and coverage fixtures. Color emoji is a planned [nearer text follow-on](../mid-term/README.md#color-emoji). Neither belongs to the current grayscale Latin/Cyrillic and bidi batch. -The [miniaudio module](../../../projects/modules/audio/miniaudio/README.md) +### Alternative audio SDK + +The [miniaudio module](../../site/modules/miniaudio.md) implements ordinary playback with an accepted Linux root-assembly WAV baseline. An alternative audio SDK/module is planned; select its SDK and assess licensing, runtime/dependency deployment, source/package integration and coexistence before @@ -221,12 +225,16 @@ define consumer/native acceptance for the chosen alternative. Spatial audio, effects graphs, capture, device enumeration/hotplug and custom codecs also need separately selected consumer requirements. -Immutable image sampling and TGUI smoothing integration are implemented in the -[sampling contract](../../rendering/pipelines-and-materials.md#immutable-sampling). -Additional atlas padding or repacking remains a separately selected extension. Current grid UVs address cell edges -without padding; the [demo samples](../../../projects/apps/demo/README.md#tile-and-sprite-samples) +### Atlas padding and repacking + +Additional atlas padding or repacking needs a separately selected scope. Preserve +the [sampling contract](../../site/rendering/pipelines-and-materials.md#immutable-sampling): +current grid UVs address cell edges without padding. The +[demo samples](../../../projects/apps/demo/README.md#tile-and-sprite-samples) retain the baseline observations. +### Game systems + Manual collision checks for object overlap/clipping are deferred to phase 1 of the [status inventory](../../STATUS.md#3-deferred). Define their shape, coordinate and boundary rules separately from physics simulation. Save-game serialization, @@ -236,32 +244,35 @@ Cheryl as a Git submodule and supply its own optional modules from the start, or implement its features directly. Do not plan on a later extraction or game refactor as a prerequisite for starting development. +### NUMA locality + NUMA discovery and memory placement are deferred indefinitely; no client/server architecture is scheduled. Reopen only for a consumer and hardware that justify multi-node locality. Any resumed facility must be optional at compile time and preserve ordinary submission/completion behavior. Separate CPU-to-node mapping from allocation, first-touch and migration; single-node runs cannot establish -multi-node acceptance. +multi-node acceptance. Preserve the +[worker provider contract](../../site/runtime/worker-execution.md#topology-providers-and-capacity). +Topology alone does not prove a speedup or guarantee residency; performance claims +require representative workload measurements. -General cache/core locality is implemented through optional hwloc support and -[application-owned worker providers](../../runtime/worker-execution.md#topology-providers-and-capacity), -separately from the NUMA deferral. Topology does not prove a speedup or guarantee -residency; performance claims still require representative workload measurements. +### Other extensions 3D, networking, world/entity/physics, serialization, device-loss recovery and broader OS/device validation remain separate consumer-driven work. Placeholders do not imply supported facilities. Establish scope, prerequisites, -ownership and acceptance before promoting any candidate into the development roadmap. +ownership and acceptance before selecting any candidate in STATUS. Broader pointer capture, modal/controller routing and clipboard/cursor/IME services -need explicit consumer requirements under the [UI capability boundaries](../../development/ui-adapters.md#routing-and-unavailable-services). -Build-tree and [prebuilt SDK consumption](../../development/consuming-engine.md#prebuilt-development-sdks) -are implemented. [CI](../../development/ci.md) -prepares compiled development libraries/headers as workflow artifacts; it publishes -no GitHub releases or demo packages. Release publication requires a separate choice -of trigger/policy and accepted platform/toolchain configurations. Pending hosted -Windows and main artifact collection remain in the active testing queue; successful -builds do not establish desktop/device acceptance. +need explicit consumer requirements under the [UI capability boundaries](../../site/development/ui-adapters.md#routing-and-unavailable-services). +Build-tree and [prebuilt SDK consumption](../../site/development/consuming-engine.md#prebuilt-development-sdks) +are implemented. SDK collection and release publication belong to the +[CI workflow](../../../.github/workflows/ci.yml); they are not a deferred engine +extension. The first main-push collection remains in the +[testing queue](../../testing-requests.md#tr20-automated-main-push-sdk-collection). +Hosted build/SDK acceptance does not establish desktop/device acceptance. + +### Monitor hotplug Native monitor hotplug is deferred in [phase 3 of the status inventory](../../STATUS.md#3-deferred); monitor hotplug and @@ -269,7 +280,7 @@ per-window scale/change reporting still need explicit consumers. Settle refreshed inventory identity, native-handle invalidation and existing-window rebinding before adding hotplug support; monitor scale alone does not satisfy a per-window service. The current -[display/window contract](../../runtime/display-and-window-contract.md) defines the +[display/window contract](../../site/runtime/display-and-window-contract.md) defines the construction-time inventory and copied-size capability limits. ### Residency accounting @@ -281,13 +292,13 @@ backend allocation, and advisory budgets from enforceable admission limits. Settle owner-thread admission and independent provider/cache ownership before allowing multiple domains. Eviction must preserve in-flight frames and avoid counting shared images/materials repeatedly. The current -[residency contract](../../resources/resource-residency.md) supplies strong ownership +[residency contract](../../site/resources/resource-residency.md) supplies strong ownership and explicit clear/replacement/teardown; cache counts do not provide byte budgets. ## Additional dungeon asset manifests Prepare one manifest for each of the two 0x72 dungeon packages in the -[download catalog](../../assets/catalog.md#packages-awaiting-manifests) when +[download catalog](../../site/assets/catalog.md#packages-awaiting-manifests) when this long-term task becomes active. Keep the original downloads available for source comparison. Preserve the existing Buch dungeon image and manifest; the added packages need distinct namespaces and image directories. @@ -301,8 +312,8 @@ Handle one package per coherent development unit: semantics rather than inferring them from dimensions alone. 3. Write a separate JSON manifest covering the selected package images and add its final locations and manifest link to the catalog. Use the current - [manifest format](../../assets/asset-manifests.md) and - [loading validation contract](../../assets/asset-loading.md). + [manifest format](../../site/assets/asset-manifests.md) and + [loading validation contract](../../site/assets/asset-loading.md). **Acceptance:** the catalog identifies each package revision, and its manifest uses valid image paths and grids; slices and animations match that revision. Schema/source checks and diff --git a/docs/planning/long-term/asset-manifest-metadata.md b/docs/planning/long-term/asset-manifest-metadata.md index 07311021..25c77992 100644 --- a/docs/planning/long-term/asset-manifest-metadata.md +++ b/docs/planning/long-term/asset-manifest-metadata.md @@ -13,7 +13,7 @@ an artwork-owner decision before additional semantics can be encoded safely. - Buch dungeon sheet: locate metadata matching the 23×24 grid of 16-pixel cells (368×384 occupied pixels), or explicitly replace the sheet and remap it. Metadata for a different revision/layout cannot safely supply this sheet's named slices. - The [download catalog](../../assets/catalog.md#dungeon-tileset) identifies its source. + The [download catalog](../../site/assets/catalog.md#dungeon-tileset) identifies its source. - Colored/Orc buildings and Mage City: add per-cell gameplay names only if the engine needs semantic lookup below the existing regional/color view level. - Character Customizer: decide whether runtime customization loads composited exports @@ -24,11 +24,11 @@ an artwork-owner decision before additional semantics can be encoded safely. geometry, but no durations or verified action names. Do not infer animation timing or material/FX intent. Invaders' draw scale is a submission setting; irregular Rover frames remain separately addressable entries. The - [recovery contract](../../assets/asset-manifests.md#recovered-college-definitions) + [recovery contract](../../site/assets/asset-manifests.md#recovered-college-definitions) records pivot evidence and retained originals. Tile selection uses the declared rules; -the [selection contract](../../assets/asset-values-and-playback.md#tile-selection) +the [selection contract](../../site/assets/asset-values-and-playback.md#tile-selection) does not infer missing artwork semantics. Additional downloaded dungeon packages await separate manifests in the [long-term plan](README.md#additional-dungeon-asset-manifests). diff --git a/docs/planning/long-term/platform-acceptance.md b/docs/planning/long-term/platform-acceptance.md index b6744840..a5899ca0 100644 --- a/docs/planning/long-term/platform-acceptance.md +++ b/docs/planning/long-term/platform-acceptance.md @@ -7,26 +7,32 @@ coverage; they are outside the active scheduled, reconcile the current source/targets/configuration first, then restore ready requests to that queue. Linux/X11 is the current native acceptance platform. Windows hosted compilation, focused tests and installed SDK consumption are -accepted within the [CI coverage limits](../../development/ci.md). +accepted within the [CI coverage limits](../../site/development/ci.md). The remaining [TR20](../../testing-requests.md#tr20-automated-main-push-sdk-collection) checks main-push SDK collection; it does not activate the desktop/controller/HID procedures here. ## Scope and prerequisites -Windows acceptance includes native input ownership and consumer/header probes, -typed-event and resize delivery, desktop resize, controller reports and HID -notification lifetime. TR3–TR5 below retain their request IDs for later activation. -They are **deferred**. The hosted MSVC configuration is accepted for the focused -CI selection; desktop and physical-device behavior remain unaccepted. +Remaining Windows acceptance covers native-window input ownership and resize +delivery, desktop resize, controller reports and HID notification lifetime. +TR3–TR5 retain their request IDs for later activation and are **deferred**. +Hosted MSVC already covers consumer/header probes, tile-animation, typed-event +delivery and the native input-lifetime unit suite in its selected configuration; +the [CI runner](../../../.github/ci/run.py) selects those owners. Treat their runs +below as baseline prerequisites for the chosen configuration, not new unfinished +features. The six native OpenGL cases in TR3 and the desktop/physical-device +observations remain outside that hosted selection. A configuration/build failure is an unresolved prerequisite, not an accepted or skipped case. Commands locate the checkout root with Git and preserve the caller's starting directory. Use a Windows Developer PowerShell with Ninja, CMake 3.28 or newer, a C++23 toolchain and initialized pinned submodules; native builds also need OpenGL -and Python with Jinja2. Engine text also requires FreeType, HarfBuzz and ICU uc/i18n; -the demo now has a builtin font fallback. Reuse a matching +and Python with Jinja2. Engine text requires FreeType and HarfBuzz; Windows uses +the operating system's ICU C API on Windows 10 version 1903 or newer, as described +in the [text dependency contract](../../site/assets/text-layout.md#supported-rendering-scope). +The demo has a builtin font fallback. Reuse a matching build directory when its compiler/configuration agree. Builds and CTest use normal priority and available parallelism. Stop on failures and require nonempty test selection without skips. Record revision, configuration, platform and coverage; @@ -36,8 +42,11 @@ automation does not establish separate desktop or physical-controller observatio Accept native input integration and resize callback cases on Windows, particularly rejection of reattachment to a different notification window and successful -reattachment to the original live window. Include tile-animation and typed-event -regressions and consumer probes on this platform. +reattachment to the original live window. Rerun the tile-animation, typed-event, +input-lifetime and consumer baseline in the selected native configuration before +the six OpenGL cases. The commands request `GAINPUT_ENABLE_HID=ON`; recheck actual +HID source/compiler selection before activation. The dependency can override that +option on Windows, and these checks do not establish HID readiness. Use a usable Windows desktop session. These native observations extend beyond the accepted hosted CI dependency/toolchain selection. @@ -126,7 +135,7 @@ try { normally. Report the platform and mode for any failure. This request does not establish per-window scale transitions or GPU reset recovery. -The reusable [desktop checks](../../development/native-desktop-checks.md) retain the +The reusable [desktop checks](../../site/development/native-desktop-checks.md) retain the Linux resize procedure. ## TR5: QA controller reports and reconnection @@ -156,7 +165,7 @@ or establish the Windows notification route. **TR6 · QA — Linux/X11 and Windows; deferred and blocked on backend work and an observation harness.** Preserve the -[single initialized native-owner contract](../../../projects/modules/platform/native-glfw/README.md#input-lifetime-and-mapping). +[single initialized native-owner contract](../../site/modules/native-glfw.md#input-lifetime-and-mapping). The [HID plan](README.md#deferred-hid-integration) owns backend corrections. Successful Gainput initialization does not prove HID readiness: HID can be compiled out, and the dependency discards its initialization return code when enabled. @@ -199,7 +208,7 @@ DirectInput fallback and notification corrections accompany future platform work macOS needs source/dependency selection and native/HID fallback ownership resolved before desktop and USB/Bluetooth controller acceptance. The current Apple source selection omits the shared HID runtime/parsers. Preserve the -[native module's capability limits](../../../projects/modules/platform/native-glfw/README.md#hid-capability-and-platform-scope) +[native module's capability limits](../../site/modules/native-glfw.md#hid-capability-and-platform-scope) until those configurations are implemented and accepted. Full Wayland and other operating systems need their own runtime acceptance for @@ -208,5 +217,5 @@ modes and their selected `CHERYL_NATIVE_INPUT` / `CHERYL_NATIVE_NULL_PLATFORM` configurations. Establish machines, fixtures and native observation requirements when activating each platform; do not infer acceptance from Linux/X11 or software-driver execution. Reuse the -[architecture procedures](../../development/architecture-validation.md) and the -[desktop checks](../../development/native-desktop-checks.md) where applicable. +[architecture procedures](../../site/development/architecture-validation.md) and the +[desktop checks](../../site/development/native-desktop-checks.md) where applicable. diff --git a/docs/planning/mid-term/README.md b/docs/planning/mid-term/README.md index dbadbbed..29b4046d 100644 --- a/docs/planning/mid-term/README.md +++ b/docs/planning/mid-term/README.md @@ -1,20 +1,23 @@ # Mid-term work Color emoji and reorder-safe sorting are planned in the -[selected roadmap](../develop-review-and-development-plan.md#selected-planned-work). +[status inventory](../../STATUS.md#2-planned). General render batching and text/timing optimization remain deferred. Sorting and optimization retain their representative-workload and settled-contract prerequisites. These tasks have no active implementation checklist yet; activate one coherent unit after its investigation/design checkpoint. Opt-in -[positioned text runs](../../assets/text-layout.md#positioned-text-runs) already +[positioned text runs](../../site/assets/text-layout.md#positioned-text-runs) already prepare glyph placements once and preserve contiguous page order. ## Color emoji -This is selected planned work beyond the current grayscale implementation. +This is selected planned work for Engine's shaped-text layout and atlas path. +The shared [font rasterizer](../../site/assets/text-layout.md#supported-rendering-scope) +already exposes alpha and premultiplied RGBA glyph pixels; that lower-level support +does not complete color-font layout, atlas submission or emoji acceptance. Select required color-font formats, fonts and emoji sequences beyond the accepted -initial [grayscale scope](../../assets/text-layout.md#native-acceptance-scope). +initial [grayscale scope](../../site/assets/text-layout.md#native-acceptance-scope). Establish RGBA page orientation, material/alpha handling and sequence/fallback policy while preserving source clusters and immutable generations. Keep grayscale text independently usable. Color glyphs diff --git a/docs/planning/short-term/README.md b/docs/planning/short-term/README.md new file mode 100644 index 00000000..a97a52f8 --- /dev/null +++ b/docs/planning/short-term/README.md @@ -0,0 +1,168 @@ +# Short-term follow-up + +This page owns unfinished regressions and dependency acceptance for implemented +font, Startup and UI behavior. The accepted Linux baseline remains in place; +these are separate gaps rather than a request to repeat all native QA. Subject +guides own current contracts, and the [testing queue](../../testing-requests.md) +owns ready acceptance requests. Each task needs its own authorized testing or +investigation phase before execution. + +## Font-selection regressions + +The [font-selection contract](../../site/assets/text-layout.md#font-selection) +and [implementation](../../../projects/engine/src/text/font-collection.cpp) +already rank discovered faces by declared weight/style. Existing discovery tests +cover lookup and ordering; registered-family CPU and GUI fixtures cover style +selection and collection face indices. The remaining task is automatic discovery +at numeric-weight boundaries. + +- [x] Implement weight/style ranking and accept the existing selection, layout + and resource checks; cover registered-family styles and face indices. +- [ ] Supply licensed fixtures with known style flags and numeric weights around + the automatic limit of 500. Verify that a non-bold face at the limit is eligible, + a face above it is excluded, and a bold flag also excludes an automatic face. +- [ ] Cover ranking around Regular (400), skipping a heavy-only automatic family, + and an asset override whose heavy face suppresses that automatic family even + when a lighter system copy exists. +- [ ] Verify that explicit preferred names, installed families and files remain + unrestricted by the automatic-weight limit. Isolate asset/system directories + from host-installed fonts and assert the selected face identity and metadata, + so an embedded fallback cannot make a failed selection look successful. + +Start with the existing +[selection fixtures](../../../projects/engine/tests/unit/src/font-selection.cpp) +and [font fixture helpers](../../../projects/engine/tests/include/testing/font-fixture.h). +Preserve fixture licensing and distinguish these cases from already covered +registered-family selection. Color emoji, wider CJK acceptance and editing/IME +remain outside the accepted [grayscale scope](../../site/assets/text-layout.md#native-acceptance-scope). + +## Startup and UI follow-up + +The [application guide](../../site/development/consuming-engine.md#command-line-startup) +owns parser, factory, result/context lifetime and option contracts. The +[adapter guide](../../site/development/ui-adapters.md#routing-and-unavailable-services) +owns complete-batch routing and focus epochs. The existing Linux root composition +has accepted native startup, field focus, placeholder and list-layout observations; +the [demo procedure](../../../projects/apps/demo/README.md#startup-and-ui-validation) +retains repeatable steps and coverage limits. + +The [Startup suite](../../../projects/modules/runtime/startup/tests/src/startup.cpp) +already covers option registration, a supplied factory's invocation and worker +count, the backend validation callback, single-use initialization, help before +creation, invalid worker count and malformed argument arrays. With OpenGL startup +configured it also covers default option help, borrowed input remaining +uninitialized on help, invalid backend selection and a zero default window width; +without that backend it covers the standard factory's rejection. The +[CI runner](../../../.github/ci/run.py) selects `tests-startup`. + +- [x] Implement common/backend Startup, complete-batch entry epochs and + `before_record` controls; add the owned RmlUi correction and supplied-Core gate. + Accept existing session/coexistence checks and independent installed-SDK Startup + first-include probes. +- [x] Add the Startup parser, supplied-factory invocation and preconstruction + validation regressions described above. +- [ ] Complete Startup factory/result handoff regressions: return a successful + context with the selected runtime policy, reject a null factory result, and + construct a runtime borrowing the successful result. The existing supplied + factory probe deliberately throws before returning a context; the standard + factory tests stop at help or invalid configuration. They do not exercise these + result branches or successful native context construction. Keep native + construction observations with the existing demo procedure. +- [ ] Add dedicated callback/batch-routing regressions. Deliver one complete + batch while application controls run before individual records; verify that + focus/visibility changes preserve the entry epoch and pending records, and + that each callback's result selects pointer delivery for that record. Existing + session/coexistence cases cover poll-latched preemption between calls; they do + not call the `before_record` overload or change focus within its batch. +- [ ] Add a placeholder hit-testing regression exercising the corrected owned + Core through public input and completed layout. Cover the state/selection cases + in the [upstream follow-up](#upstream-placeholder-follow-up). Existing editing + tests and accepted demo observations do not provide that dedicated regression. +- [ ] Complete the supplied dependency acceptance below with an independent host. + +### Supplied RmlUi acceptance + +Obtain a corrected RmlUi 6.3 target/package and a consuming host that bypass +Cheryl's source helper. Follow the full +[dependency contract](../../site/modules/rmlui.md#dependency-configuration), including +Engine font services and rasterizer ABI compatibility as well as the +[placeholder correction declaration](../../site/modules/rmlui.md#placeholder-dependency-contract). +Metadata must describe the actual library; setting a property does not correct it. + +Accept the selected supplied-target or installed-package composition, resolving +aliases before declaring properties. Check rejection when the required correction +declaration is absent or false, independently of successful Cheryl-owned source +configuration. Exercise the supplied library's placeholder focus, editing and +shutdown in the consuming host. Record which supplied composition was exercised; +configuration rejection and downstream-owned native success do not establish +supplied-binary behavior. + +## Upstream placeholder follow-up + +The [RmlUi guide](../../site/modules/rmlui.md#placeholder-dependency-contract) +owns the source invariant and Cheryl's downstream correction. The affected pin is +RmlUi 6.3, `ba95ffe8bfb6370efb2cdcca927eaad4710c5413`. Source inspection establishes +a placeholder/display-length mismatch; it does not identify the instruction that +caused the original reported crash. No unmodified-upstream reproduction, +pre-fix sanitizer evidence or upstream submission has been completed for this task. + +- [ ] Reproduce in a separate, unmodified upstream checkout, preserving shared + `extern/rmlui`. Start at the pin, then check current upstream and record each full + revision and actual runtime configuration. A guarded Cheryl build cannot serve + as the pre-fix case. Recheck existing issues before proposing a submission. +- [ ] Load a real font, show the document, complete `Context::Update()` and verify + a valid face, positive field width and formatted lines. Send mouse movement + before button down/up through the public context API. Separate ordinary clicks + in manual time unless deliberately testing double-clicks. +- [ ] Capture a demonstrably failing pre-fix regression, with a stack/memory + diagnostic or another observable failure, then show the same case passing after + the C++ correction. A value-stays-empty assertion alone can miss the invalid + read. Include a long ASCII hint (for example 256 characters), verify its actual + formatted length and click toward the right; a short hint need not cross a + sanitizer red zone. +- [ ] Inspect logical value/display sizes, line offsets/lengths and iterator range + without dereferencing an invalid range for logging. Distinguish this mismatch + from absent-font or empty-line failures. Later cursor clamping cannot undo an + earlier invalid read. +- [ ] Review the guard's placement: Cheryl returns before resetting + `ideal_cursor_position_to_the_right_of_cursor`. Follow relative indices through + absolute cursor/selection updates, especially later textarea lines. Correct + this in upstream C++, with an upstream regression; the CMake source-copy helper + remains downstream integration. +- [ ] Complete the cases below, run the selected regression and full upstream + unit suite with AddressSanitizer/UndefinedBehaviorSanitizer, and check that the + selected cases actually ran. Choose a compatible configuration from the pinned + [sanitizer workflow](../../../extern/rmlui/.github/workflows/build.yml) and + [unit-test target](../../../extern/rmlui/Tests/Source/UnitTests/CMakeLists.txt). + Record compiler/standard library, font engine, backend and runtime mode; + unexecuted configurations remain unverified. +- [ ] Check current contribution and [AI policy](https://github.com/mikke89/RmlUi/blob/master/AI_POLICY.md) + before any external submission. The source analysis and Cheryl guard were + AI-authored. A human submitter must personally understand, reproduce, review and + test the change, author required human communication, and disclose the actual + AI contribution/model as required. Do not repurpose an AI-written issue draft + as human-authored communication. No upstream contact is part of this plan's + maintenance. + +### Required placeholder cases + +Use the existing [input unit-test fixture](../../../extern/rmlui/Tests/Source/UnitTests/ElementFormControlInput.cpp) +and [text-input benchmark](../../../extern/rmlui/Tests/Source/Benchmarks/WidgetTextInput.cpp) +for public event submission, selection inspection and time advancement. The +existing `form.input.text.no_font` case covers a different failure. + +| Case | Required observation | +| --- | --- | +| Short/long ASCII hints; left/middle/right clicks | Focus works; editable value, caret and selection endpoints remain empty/zero; no invalid access. The long case demonstrates pre-fix failure. | +| Drag, Shift-click and double-click on a hint | Hint never becomes selected or editable text. | +| Type, clear and click again | Only typed text becomes the value; clearing restores safe placeholder behavior, including retained cursor state. | +| Nonempty value; empty field without a hint | Existing caret placement and selection still work. | +| Multibyte UTF-8 hint and entered value | Hint stays outside the editable value; entered-text character boundaries remain valid. | +| Left, center and right alignment | Visible caret uses the empty value's aligned insertion position. | +| Wrapped/newline textarea hints; later-line clicks and vertical movement | No invalid range; final absolute caret/selection endpoints are zero and selected text is empty. | + +The [test shell](../../../extern/rmlui/Tests/Source/Common/TestsShell.cpp) performs +font loading/layout with a dummy renderer by default; `RMLUI_TESTS_USE_SHELL` +selects its native backend. After logic/sanitizer checks, separately observe real +focus, visible caret, placeholder appearance and cleanup in native mode. A +headless pass does not establish those visual results. diff --git a/docs/planning/short-term/unicode-text.md b/docs/planning/short-term/unicode-text.md deleted file mode 100644 index ef7ecf8a..00000000 --- a/docs/planning/short-term/unicode-text.md +++ /dev/null @@ -1,41 +0,0 @@ -# Unicode text layout and native acceptance - -The initial grayscale [Unicode scope](../../assets/text-layout.md#native-acceptance-scope) -has an accepted Linux native baseline for appearance, alignment, fallback and -local-width wrapping. Dedicated font-style/weight fixtures and regressions remain -unfinished below. The subject guide owns font, layout and resource contracts and -the rationale for a separate service. Color emoji, wider CJK acceptance and -editing/IME remain outside this task. - -## Progress - -- [x] Add owned UTF-8 scalar/source records with maximal-subpart replacement and - preserved NUL/BOM/unassigned scalars. -- [x] Add decoder regressions and a neutral first-include probe. -- [x] Make STBFont consume scalars while preserving ASCII metrics and FFont's byte API. -- [x] Cover malformed/multilingual fallback and CPU submission. -- [x] Settle script, direction, wrapping, fallback and grayscale scope. -- [x] Add immutable file/family selection, inspected coverage and licensed embedded - fallback through private FreeType implementation. -- [x] Add HarfBuzz shaping and ICU bidi/grapheme/line boundaries with owned clusters, - face-qualified glyphs and measured accepted-line widths. -- [x] Prepare CPU grayscale pages and upload complete immutable retained generations; - failure preserves the preceding application generation. -- [x] Integrate worker preparation/platform uploads into the builtin demo, with - multilingual, wrapping, failure/retention checks and a visual harness. -- [x] Correct bidi-control source mapping while preserving shaping joiners/selectors. -- [x] Accept Linux Release Engine-only decoder, selection, shaping/bidi/wrapping, - resource/submission and header coverage. -- [x] Compile the native Linux/X11/OpenGL demo with HID and both UI adapters disabled. -- [x] Rank installed faces by declared weight/style and exclude heavy automatic defaults. -- [x] Accept existing font/layout/resource checks after the font-style selection changes - in the Linux Release Engine-only selection. -- [ ] Supply licensed style/weight fixtures and dedicated regressions for the - automatic limit, heavy-only family skipping and unrestricted explicit preferences - in separately authorized testing phases. Existing family cases cover baseline - discovery/order rather than those numeric-weight boundaries. -- [x] Accept reported sequential/concurrent visual, fallback and replacement - observations within the [native scope](../../assets/text-layout.md#native-acceptance-scope). - -Possible [color emoji and measured layout/cache work](../mid-term/README.md) -follow this task without changing its acceptance scope. diff --git a/docs/api/index.md b/docs/site/api/index.md similarity index 77% rename from docs/api/index.md rename to docs/site/api/index.md index 868a1d2f..6fbe038b 100644 --- a/docs/api/index.md +++ b/docs/site/api/index.md @@ -5,6 +5,13 @@ It covers Cheryl-owned Engine headers, public Startup/Workers modules, and the GLFW, OpenGL, TGUI, RmlUi and miniaudio modules. Module availability depends on the targets enabled in your build. +For a known symbol, use **Symbols** below or search its qualified name. For an +operation you have not chosen an API for, start with the +[Engine Systems tables](../engine-systems/index.md#find-a-system) or the +[task index](../getting-started/how-to/index.md), then return here for the exact +contract. [Migration](#migration) is for existing consumers moving from the older +names and binary boundaries; new projects can use the current entry points. + ```{toctree} :maxdepth: 2 @@ -42,7 +49,7 @@ portable facilities are still provided by `Cheryl::Engine`. ## Entry points -The [Engine Systems tables](../engine-systems/index.md#reading-order) group the +The [Engine Systems tables](../engine-systems/index.md#find-a-system) group the public types and important enums by task, with detailed contract links. For physical control names, start with [Input controls](../engine-systems/input.md#controls-for-bindings); for backend implementations, start with the @@ -55,14 +62,19 @@ explains source-checkout and SDK target selection. | Task | Entry point | Declaring header | | --- | --- | --- | -| Configure application startup | {cpp:class}`CE::Application::Startup` | [startup.h](../../projects/modules/runtime/startup/include/cheryl/application/startup.h) | -| Implement game hooks | {cpp:struct}`CE::Game::AbstractGame` | [abstract-game.h](../../projects/engine/include/cheryl/core/game-framework/abstract-game.h) | -| Choose physical input controls | {cpp:enum}`CE::Input::KeyboardKey`, {cpp:enum}`CE::Input::MouseButton`, {cpp:enum}`CE::Input::GamepadControl` | [button-codes.h](../../projects/engine/include/cheryl/core/controls/button-codes.h) | -| Prepare and upload assets | {cpp:struct}`CE::Assets::Loading::Loader` | [asset-loader.h](../../projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h) | -| Select immutable font data | {cpp:class}`CE::Text::FontCollection` | [font-collection.h](../../projects/engine/include/cheryl/text/font-collection.h) | -| Rasterize toolkit font glyphs on CPU | {cpp:class}`CE::Text::FontRasterizer` | [font-rasterizer.h](../../projects/engine/include/cheryl/text/font-rasterizer.h) | -| Retain sprite resources | {cpp:struct}`CE::Assets::Sprites::Asset` | [sprite.h](../../projects/engine/include/cheryl/assets/types/2d/sprite.h) | -| Find loaded sprites | {cpp:struct}`CE::Assets::Sprites::Manager` | [sprite-mgr.h](../../projects/engine/include/cheryl/core/resources/asset-management/sprite-mgr.h) | +| Configure application startup | {cpp:class}`CE::Application::Startup` | [startup.h](../../../projects/modules/runtime/startup/include/cheryl/application/startup.h) | +| Implement game hooks | {cpp:struct}`CE::Game::AbstractGame` | [abstract-game.h](../../../projects/engine/include/cheryl/core/game-framework/abstract-game.h) | +| Choose physical input controls | {cpp:enum}`CE::Input::KeyboardKey`, {cpp:enum}`CE::Input::MouseButton`, {cpp:enum}`CE::Input::GamepadControl` | [button-codes.h](../../../projects/engine/include/cheryl/core/controls/button-codes.h) | +| Attach reusable per-consumer action bindings | {cpp:class}`CE::Input::Actions::ContextSubmission`, {cpp:class}`CE::Input::Actions::Context` | [action-context.h](../../../projects/engine/include/cheryl/core/controls/action-context.h); [action contexts](../runtime/input-state-model.md#action-contexts) | +| Prepare and upload assets | {cpp:struct}`CE::Assets::Loading::Loader` | [asset-loader.h](../../../projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h) | +| Configure the application's default and named fonts | {cpp:class}`CE::Text::FontService`, through `Context::fonts()` | [font-service.h](../../../projects/engine/include/cheryl/text/font-service.h); [configuration](../assets/text-layout.md#configure-fonts-once) | +| Select, save and restore a font choice | {cpp:class}`CE::Text::Font` and `FontService::choose_random`/`restore_or_choose` | [font.h](../../../projects/engine/include/cheryl/text/font.h); [saved choices](../assets/text-layout.md#choose-and-save-a-font) | +| Load a separate ordered font snapshot | {cpp:class}`CE::Text::FontCollection` | [font-collection.h](../../../projects/engine/include/cheryl/text/font-collection.h) | +| Retain and asynchronously replace a changing text label | {cpp:class}`CE::Text::Rendering::Updater`, {cpp:struct}`CE::Text::Rendering::Request` | [text-updater.h](../../../projects/engine/include/cheryl/text/text-updater.h); [retained text updates](../assets/text-layout.md#retained-text-updates) | +| Rasterize toolkit font glyphs on CPU | {cpp:class}`CE::Text::FontRasterizer` | [font-rasterizer.h](../../../projects/engine/include/cheryl/text/font-rasterizer.h) | +| Retain sprite resources | {cpp:struct}`CE::Assets::Sprites::Asset` | [sprite.h](../../../projects/engine/include/cheryl/assets/types/2d/sprite.h) | +| Draw a whole PNG without a sprite manifest | {cpp:struct}`CE::Assets::Graphics::Asset` | [graphic.h](../../../projects/engine/include/cheryl/assets/types/2d/graphic.h); [whole-image sequence](../getting-started/how-to/draw-sprite.md#draw-a-whole-image) | +| Find loaded sprites | {cpp:struct}`CE::Assets::Sprites::Manager` | [sprite-mgr.h](../../../projects/engine/include/cheryl/core/resources/asset-management/sprite-mgr.h) | See {cpp:enum}`CE::Runtime::Simulation::UpdateKind` for simulation update kinds, {cpp:type}`CE::Text::FontSource` for font selection sources, and diff --git a/docs/site/architecture/assembling-engine.md b/docs/site/architecture/assembling-engine.md new file mode 100644 index 00000000..e79861e7 --- /dev/null +++ b/docs/site/architecture/assembling-engine.md @@ -0,0 +1,41 @@ +--- +orphan: true +--- + +# Assembling the Engine + +Application composition is now part of +[Source and SDK integration](../development/consuming-engine.md). This page preserves +the previous URL and section bookmarks. + +## Facades and public includes + +See [public target selection](../development/consuming-engine.md#granular-composition) +and [Dependencies and headers](../development/consuming-engine.md#dependencies-and-headers). + +## Factories and context composition + +See [Context assembly](../development/consuming-engine.md#context-assembly) and +[Command-line startup](../development/consuming-engine.md#command-line-startup). + +## Backend boundaries + +See [Context assembly](../development/consuming-engine.md#context-assembly), +[Engine Systems](../engine-systems/index.md) and +[Integration contracts](interfaces-adapters.md). + +## Using the assembled context + +See [Context assembly](../development/consuming-engine.md#context-assembly) and +[asset preparation/publication](../assets/asset-loading.md#runtime-entry-point). + +## Execution and shutdown + +See [Ownership and cleanup](ownership-cleanup.md#shut-down-in-dependency-order), +[Threads and data flow](threads-data-flow.md) and +[the runtime lifecycle](../runtime/runtime-frame-boundary.md). + +## Current limits + +See each subject guide's current limits and the +[capability inventory](../../STATUS.md). diff --git a/docs/site/architecture/index.md b/docs/site/architecture/index.md new file mode 100644 index 00000000..fc5776c5 --- /dev/null +++ b/docs/site/architecture/index.md @@ -0,0 +1,19 @@ +--- +orphan: true +--- + +# Architecture + +Architecture guidance now lives with its readers' tasks: + +- [Source and SDK integration](../development/consuming-engine.md) covers public + targets, headers and compatible application context assembly. +- [Ownership and cleanup](ownership-cleanup.md) and + [Threads and data flow](threads-data-flow.md) explain application lifetime and + execution decisions. +- [Integration contracts](interfaces-adapters.md) and + [Module authoring](../development/modules.md) cover implementing integrations. +- [Engine Systems](../engine-systems/index.md) maps public types and relationships; + [Guides by topic](../guides/index.md) owns the detailed procedures. + +This page preserves the previous overview URL. diff --git a/docs/architecture/interfaces-adapters.md b/docs/site/architecture/interfaces-adapters.md similarity index 91% rename from docs/architecture/interfaces-adapters.md rename to docs/site/architecture/interfaces-adapters.md index fb3067e5..df556ebc 100644 --- a/docs/architecture/interfaces-adapters.md +++ b/docs/site/architecture/interfaces-adapters.md @@ -1,4 +1,5 @@ -# Interfaces and Adapters +(interfaces-and-adapters)= +# Integration contracts Use this catalogue when implementing or replacing an integration. A runtime context needs five compatible adapters: display, input, presentation, renderer @@ -10,10 +11,10 @@ interface obligations, not a requirement for five libraries. The | Contract | Existing implementation | Obligations and detailed contract | | --- | --- | --- | -| {cpp:class}`CE::Platform::DisplaySystem` | `CE::Platform::GLFW::DisplaySystem` | Own windows, expose monitor inventory and scale, create/select a compatible active window; document borrowed inventory freshness. [Display ownership](../runtime/display-and-window-contract.md); [Native GLFW](../modules/native-glfw.md). | +| {cpp:class}`CE::Platform::DisplaySystem` | `CE::Platform::GLFW::DisplaySystem` | Own windows, expose monitor inventory and scale, create/select a compatible active window; document borrowed inventory freshness. [Display ownership](../runtime/display-and-window-contract.md); [Native GLFW](../modules/native-glfw.md); [platform replacement and current OpenGL coupling](#replacing-a-platform-integration). | | {cpp:class}`CE::Platform::Window` | `CE::Platform::GLFW::Window` | Report logical size, drawable pixels, mode and close state; implement resize/mode/cursor operations and surface deferred native failures. The display owns the window. [Sizes, modes and observation](../runtime/display-and-window-contract.md#sizes-modes-and-observation); [native callback failures](../runtime/failure-reporting.md#native-callbacks). | | {cpp:class}`CE::Input::System` | `CE::Platform::GLFW::InputSystem` | Attach/detach from a live window, identify devices, normalize standard controls to Engine IDs, own bindings and publish complete polls. Declare Events, Text and focus support; latch supported capture/routing before collection. [Input adapter publication](../runtime/input-state-model.md#adapter-publication); [Native GLFW](../modules/native-glfw.md). | -| {cpp:class}`CE::Rendering::PresentationSurface` | `CE::Rendering::OpenGL::GLFWContext` | Present on the platform/presentation owner while its borrowed window and graphics dependencies remain live. Backend-specific context operations extend this contract. [Runtime ownership](assembling-engine.md); [OpenGL](../modules/opengl.md). | +| {cpp:class}`CE::Rendering::PresentationSurface` | `CE::Rendering::OpenGL::GLFWContext` | Present on the platform/presentation owner while its borrowed window and graphics dependencies remain live. Backend-specific context operations extend this contract. [Context assembly](../development/consuming-engine.md#context-assembly); [OpenGL](../modules/opengl.md). | | {cpp:class}`CE::Rendering::Renderer` | `CE::Rendering::OpenGL::Renderer` | Initialize/close graphics, consume complete ordered frames, apply viewport/camera/draw state and maintain deferred resources even without a new frame. [Frame publication](../runtime/runtime-frame-boundary.md#frame-publication-and-recycling); [native retirement](../resources/resource-residency.md#native-retirement-and-maintenance). | | {cpp:struct}`CE::Rendering::Resources::Provider` | `CE::Rendering::OpenGL::ResourceProvider` | Upload images, alpha atlases and 2D geometry, and link executable programs. Copy transient CPU inputs; return retained immutable generations governed by one backend domain. [Publication and retirement](../resources/resource-residency.md); [OpenGL](../modules/opengl.md). | @@ -56,9 +57,10 @@ For an SDL3 replacement, the platform work maps to `DisplaySystem`, `Window` and `Input::System`. Keeping Cheryl's OpenGL renderer also requires an OpenGL `Context` bridge and compatible resource lifetime. The current OpenGL target and startup factory select Native GLFW explicitly; a new platform therefore needs -composition/build selection changes as well as interface implementations. The -[module selection](assembling-engine.md), [OpenGL owner](../modules/opengl.md) and -[startup guide](../modules/startup.md) describe that current coupling. +composition/build selection changes as well as interface implementations. +[Application composition](../development/consuming-engine.md#context-assembly), +the [OpenGL owner](../modules/opengl.md) and the [Startup guide](../modules/startup.md) +describe that current coupling. Keyboard, mouse-button and gamepad enums belong to Engine and are accepted directly by bindings; see @@ -80,7 +82,9 @@ selection still determines available devices and observations. initialization dependencies. Use the existing module's independent consumer and the architecture validation procedures as conformance examples. -The [assembly guide](assembling-engine.md) explains factories and facades; -[module authoring](../development/modules.md) covers target registration and -independent consumers. Runtime-owned and application-owned objects follow +[Source and SDK integration](../development/consuming-engine.md) explains application +factories and public targets; [Module authoring](../development/modules.md) covers +target registration and independent owners. Validate the replacement with +[independent consumers and header probes](../development/architecture-validation.md#independent-consumers-and-header-probes). +Runtime-owned and application-owned objects follow [ownership and cleanup](ownership-cleanup.md). diff --git a/docs/architecture/ownership-cleanup.md b/docs/site/architecture/ownership-cleanup.md similarity index 97% rename from docs/architecture/ownership-cleanup.md rename to docs/site/architecture/ownership-cleanup.md index 4407db4c..c17225b9 100644 --- a/docs/architecture/ownership-cleanup.md +++ b/docs/site/architecture/ownership-cleanup.md @@ -53,7 +53,7 @@ CPU release state can have a different lifetime. Pooled object handles retain their return/construction contexts, allowing final release after a pool facade dies. Their own callers still need to finish before that facade is destroyed. Use [CPU memory lifetime](../resources/resource-lifetime.md) when allocating -application objects, and the backend's native retirement contract for GPU handles. +application objects, and [native retirement and maintenance](../resources/resource-residency.md#native-retirement-and-maintenance) for GPU handles. ## Shut down in dependency order diff --git a/docs/architecture/threads-data-flow.md b/docs/site/architecture/threads-data-flow.md similarity index 93% rename from docs/architecture/threads-data-flow.md rename to docs/site/architecture/threads-data-flow.md index 92c8ae93..7644b084 100644 --- a/docs/architecture/threads-data-flow.md +++ b/docs/site/architecture/threads-data-flow.md @@ -52,8 +52,11 @@ modifying simulation through a captured `this`. Simulation checks readiness befo calling `get()` and handles any future error according to the game's failure policy. Bound outstanding replacements to avoid an ever-growing queue of obsolete work. -For [text replacement](../getting-started/how-to/update-text.md), old frames retain -their matching text geometry and atlas while a newer generation is adopted. For +For a worked implementation of this sequence, use +[retained text replacement](../getting-started/how-to/update-text.md). Its +`CE::Text::Rendering::Updater` owns preparation, upload and adoption; the game +provides new content and decides how to report or retry a failure. Old frames +retain their matching text geometry and atlas while a newer generation is adopted. For [asset batches](../assets/asset-loading.md#publication-and-retry), upload can publish entries incrementally; a failed batch does not roll back earlier publications. Choose the application's adoption boundary separately from the cache's behavior. diff --git a/docs/assets/asset-loading.md b/docs/site/assets/asset-loading.md similarity index 88% rename from docs/assets/asset-loading.md rename to docs/site/assets/asset-loading.md index dc27f7a2..cf79a121 100644 --- a/docs/assets/asset-loading.md +++ b/docs/site/assets/asset-loading.md @@ -9,15 +9,27 @@ publishes retained metadata on the provider's loading thread. ## Runtime entry point -After initializing the selected backend, load a root with its provider: +Start with the self-contained [First Game asset root](../getting-started/first-game.md#init-load-assets-and-create-the-actor). +Here `asset_root` names your copy of that directory and `engine` is the initialized +runtime context. Include ``, +`` and ``. +During platform-owned game initialization, load the root with its provider: ```cpp -CE::Assets::Loading::Loader loader("assets"); +CE::Assets::Loading::Loader loader(asset_root); loader.load_assets(engine.resources()); -auto actor = CE::Assets::Sprites::Manager::get().get_asset("miniworld:soldier_swordsman_cyan"); +auto actor = CE::Assets::Sprites::Manager::get().get_asset("first-game", "actor"); +if (!actor) + throw std::runtime_error("The actor sprite is required"); auto documents = loader.manifests(); // shared_ptr> ``` +The expected key is `first-game:actor`; the handle retains the image, cell geometry +and compatible default material. Continue with [sprite submission](../getting-started/first-game.md#prepare-a-frame-publish-the-actors-current-appearance) +to display it. Optional repository artwork needs the separate +[package installation](catalog.md) and [focused index selection](catalog.md#load-only-installed-packages) +described in the catalog; loading the entire repository root can select absent images. + Each asset family owns its definitions, retained values and cache API. Sprite and tileset managers expose `load()` for definitions; the image manager exposes `load()` for paths and `upload()` for decoded pixels. Their inherited `get_asset()` @@ -145,7 +157,8 @@ retrieves an already-initialized singleton. ## Resources and application bootstrap The generic loader loads indexed shaders/materials, images, sprites and tilesets. -The application still chooses fonts explicitly; the +Fonts use the separate [configured font service](text-layout.md#configure-fonts-once), +whose default is embedded when the application supplies no configuration. The [file/font discovery guide](file-and-font-discovery.md) defines candidate selection, index freshness and the legacy font cache's default/collection-face policy. The demo selects `graphics/shaders/graphics-manifests.json` and retrieves `main:text` and `main:images` diff --git a/docs/assets/asset-manifests.md b/docs/site/assets/asset-manifests.md similarity index 82% rename from docs/assets/asset-manifests.md rename to docs/site/assets/asset-manifests.md index 475cd467..0687bd76 100644 --- a/docs/assets/asset-manifests.md +++ b/docs/site/assets/asset-manifests.md @@ -1,17 +1,18 @@ # Graphics manifests 2.0 Graphics indexes and definition documents use version `2.0`. The authoritative -schemas are [graphics indexes](../../assets/schemas/graphics-manifest-index-2.0.schema.json), -[sprites/tilesets](../../assets/schemas/graphics/sprite-tileset-manifest-2.0.schema.json) -and [shaders/materials](../../assets/schemas/graphics/shader-manifest-2.0.schema.json). +schemas are [graphics indexes](../../../assets/schemas/graphics-manifest-index-2.0.schema.json), +[sprites/tilesets](../../../assets/schemas/graphics/sprite-tileset-manifest-2.0.schema.json) +and [shaders/materials](../../../assets/schemas/graphics/shader-manifest-2.0.schema.json). Earlier versions are rejected rather than implicitly upgraded. `$schema` is an editor hint; the runtime neither fetches it nor discovers schemas as assets. ## Add a sprite sheet -Start with the maintained [First Game asset root](../../projects/apps/first-game/assets/). -Copy that directory into your application's assets so its image, shader sources -and material recipe stay together. Its relevant layout is: +Start with the maintained [First Game asset root](../../../projects/apps/first-game/assets). +Copy that directory into your application's assets so its image and sprite +definition stay together. Ordinary drawing uses a provider-supplied material; +its relevant layout is: ```text assets/ @@ -19,20 +20,17 @@ assets/ graphics-manifests.json definitions/sprites.json textures/actor.png - shaders/materials.json - shaders/first-game.vert - shaders/first-game.frag ``` -The complete index selects the sprite definition and its drawing materials: +The complete index selects the sprite definition: -```{literalinclude} ../../projects/apps/first-game/assets/graphics/graphics-manifests.json +```{literalinclude} ../../../projects/apps/first-game/assets/graphics/graphics-manifests.json :language: json ``` The complete sprite document describes the actor's two 16-by-16 cells: -```{literalinclude} ../../projects/apps/first-game/assets/graphics/definitions/sprites.json +```{literalinclude} ../../../projects/apps/first-game/assets/graphics/definitions/sprites.json :language: json ``` @@ -51,16 +49,20 @@ if (!actor) Include the loader and sprite-manager headers plus ``; `asset_root` is the copied assets directory and `engine` is the initialized runtime context. -The [sprite task](../getting-started/how-to/draw-sprite.md) supplies the matching -material and frame submission. Cell zero selects the left frame and cell one +The [First Game sprite walkthrough](../getting-started/first-game.md#put-a-sprite-on-screen) supplies frame +submission using the asset's retained material. Its +[whole-image alternative](../getting-started/how-to/draw-sprite.md#draw-a-whole-image) +needs no sprite manifest. Cell zero selects the left frame and cell one the right. The image must contain both cells; preparation rejects a grid whose occupied bounds exceed its decoded image. To add your own sheet, copy a definition entry, choose a unique namespace/name, and change its texture path, frame size, rows, columns, spacing and pivot. Add the document to the index. If retrieval returns null, check the qualified identity and -index selection; unlisted JSON is not loaded. Keep `shaders/materials.json` and -its source files when using the maintained image/text materials. The field +index selection; unlisted JSON is not loaded. This asset root needs no shader +documents or shader source files: its sprite and uploaded text retain the +[provider's default materials](../rendering/pipelines-and-materials.md#default-asset-materials). +Add shader/material documents only when selecting custom recipes. The field contracts below cover larger atlases, clips, autotiles and custom shaders. ## Index selection and paths @@ -79,8 +81,8 @@ registered names; absolute paths, parent traversal, backslashes and colons rejec The checked-in layout separates `graphics/definitions/`, `graphics/textures/`, `graphics/shaders/` and `graphics/ui/`. The main -[graphics index](../../assets/graphics/graphics-manifests.json) selects sprite/tileset -definitions; the [shader index](../../assets/graphics/shaders/graphics-manifests.json) +[graphics index](../../../assets/graphics/graphics-manifests.json) selects sprite/tileset +definitions; the [shader index](../../../assets/graphics/shaders/graphics-manifests.json) selects only the main demo recipes. The [asset package catalog](catalog.md) lists download sources and image placement @@ -93,8 +95,9 @@ for the packages referenced by the checked-in manifests. - An entry's `pivot` overrides `defaults.sprite.pivot` or `defaults.tileset.pivot` according to the containing map. - Sprite/tileset documents declare `asset_class: "sprite-tileset"`. An entry's optional `shader` overrides the document-level selection. It is a qualified - material ID, independent of the texture path; omitted selections use a supplied - draw-style material. An explicit draw-style material overrides an asset selection. + material ID, independent of the texture path. When omitted, sprite/tileset loading + obtains and retains the provider's compatible default material. An explicit + draw-style material overrides either that default or a manifest selection. - Shader and material are one asset class, declared as `asset_class: "shader"`. Their program and material names share qualified identity validation and cannot collide with another selected program/material identity. @@ -204,7 +207,7 @@ A shader document contains named `programs` and `materials`. Each program owns `stages.vertex` and `stages.fragment` source references. Each material names its qualified `program`, `vertex_layout`, `topology`, typed `parameters` and backend `bindings`; optional fields are `state`, literal `defaults` and FX `sampling`. -See [main.json](../../assets/graphics/shaders/main.json) for the complete text/image +See [main.json](../../../assets/graphics/shaders/main.json) for the complete text/image example: both materials share one program but use different draw topologies. Definitions contain no texture paths or default images. Textures contain no shader @@ -230,13 +233,15 @@ text/image materials are indexed; the other shaders remain manual, unverified examples. TGUI and RmlUi select [provider-owned default materials](../rendering/pipelines-and-materials.md#default-ui-materials) automatically; custom UI materials can use the same indexed recipes described here. +The [custom sprite material example](../rendering/pipelines-and-materials.md#give-one-object-a-custom-material) +supplies a complete ordinary-object recipe, shader sources and per-draw parameters. ## Recovered college definitions -[Invaders](../../assets/graphics/definitions/misc/invaders.json), -[HyperMaze](../../assets/graphics/definitions/misc/hypermaze.json), -[Rover](../../assets/graphics/definitions/misc/rover.json) and -[Tileset](../../assets/graphics/definitions/tilesets/tileset.json) preserve the legacy +[Invaders](../../../assets/graphics/definitions/misc/invaders.json), +[HyperMaze](../../../assets/graphics/definitions/misc/hypermaze.json), +[Rover](../../../assets/graphics/definitions/misc/rover.json) and +[Tileset](../../../assets/graphics/definitions/tilesets/tileset.json) preserve the legacy source ordering and pixel rectangles. Invaders has eleven sections containing nineteen frames; Rover uses four separate entries because its frame sizes differ. HyperMaze has five columns and six rows; Tileset has four cells. All are indexed. diff --git a/docs/assets/asset-values-and-playback.md b/docs/site/assets/asset-values-and-playback.md similarity index 91% rename from docs/assets/asset-values-and-playback.md rename to docs/site/assets/asset-values-and-playback.md index fd0ab7af..9ab9896b 100644 --- a/docs/assets/asset-values-and-playback.md +++ b/docs/site/assets/asset-values-and-playback.md @@ -21,7 +21,7 @@ Asset families use contextual names: qualify `Asset`, `Data`, `Definition` and | `CE::Assets::Tilesets` | `Definition` and `Clip` own metadata; `Asset`, `Tile` and `Animation` retain resources; `Selection` owns CPU rule sampling; `Manager` caches tilesets. | | `CE::Assets::Grids` | `Definition`, `View` and `CellIndex` describe addressing; `GeometryData` and `make_geometry()` provide CPU upload storage. | | `CE::Assets::Animation` | `TimedFrame` is shared clip metadata; `Cursor` supplies ordinal wrap/clamp playback. | -| `CE::Assets::Graphics` | `Asset` retains a whole-image quad and its image. | +| `CE::Assets::Graphics` | `Asset` retains a whole-image quad, its image and selected material. | Pixel coordinates, vertex shapes and the uploaded `Buffer2D` contract belong to `CE::Rendering::Geometry`. Image/program/material interfaces belong to @@ -53,9 +53,13 @@ texture coordinates. Pivot formulas and named views are defined by the ## Resources and caches `CE::Assets::Content2D` retains image, geometry and an optional selected material generation. -Submission uses an explicit draw-style material first, then the asset selection; -it rejects when neither supplies a usable material. Tile and animation handles -retain the same selection. Direct construction permits null +Sprite/tileset loading supplies a provider default when the definition names no +material; whole-image graphic creation does the same unless given an explicit +handle. These [ordinary defaults](../rendering/pipelines-and-materials.md#default-asset-materials) +match the existing triangle/strip geometry and need no application shaders. +Submission uses an explicit draw-style material first, then the retained asset +selection; it rejects when neither supplies a usable material. Tile and animation +handles retain the same selection. Direct construction permits null handles and does not prove matching layout, grid bounds or backend domains. Loader construction supplies validated definitions and uploaded resources; CPU submission validates its selected geometry/material/range. Read-only resource metadata can be @@ -65,9 +69,11 @@ or backend teardown, but cannot perform native operations through a closed domai See [resource ownership and retirement](../resources/resource-residency.md). `CE::Assets::Graphics::Asset::from_image()` rejects a null or zero-sized image, then uploads a six-vertex -whole-image quad on the provider owner. The caller must use a compatible image and +whole-image quad and retains its selected material on the provider owner. The caller must use a compatible image and provider domain. `load()` first decodes/uploads the image. Failure returns no graphic asset; these helpers do not publish a cache entry. +The [whole-image sequence](../getting-started/how-to/draw-sprite.md#draw-a-whole-image) +shows loading a background PNG, positioning it, submitting it and releasing it. Sprite and tileset managers use `namespace:name` IDs, require referenced images to be loaded, and retain existing IDs. `CE::Assets::Images::Manager` uses lexically normalized paths; @@ -80,6 +86,10 @@ strong handles under a lock; those handles survive clear/replacement independent ## Sprite playback +For a first animated actor, [Play sprite animation](../getting-started/how-to/animate-sprite.md) +adds a timed clip to First Game, retains its cursor, advances it during update +and submits its selected cell. The rules below define lookup and playback limits. + `CE::Assets::Sprites::Asset` retains one immutable definition snapshot and indexes clips by name/facing. Construction checks a nonempty grid and nonempty clips with positive millisecond durations and in-range cells, rejecting duplicate clip keys. It does not revalidate @@ -318,16 +328,21 @@ callback effects intact. Alternate-bank layout is rejected. BitmapFont retains a The separate [Unicode text service](text-layout.md) shapes accented Latin/Cyrillic, resolves paragraph bidi and optional local-width wrapping, selects whole-grapheme fallback and prepares immutable glyph pages. Its `CE::Text::Rendering::Generation` overload submits -already uploaded placements and matching page handles; supply `context.image` for -page selection. Legacy Font implementations retain their existing APIs/metrics. +already uploaded placements and matching page handles, with a provider default +material retained during upload. AsciiFont loading also retains a default; +explicit upload/load material handles and per-draw materials remain available. +Legacy Font implementations retain their existing metrics. Deprecated BitmapFont +keeps its explicit material-selection path. `CE::Rendering::Submission::resolve_sprite`, `CE::Rendering::Submission::resolve_tile`, `CE::Rendering::Submission::resolve_graphic` and `CE::Rendering::Submission::resolve_text` perform CPU submission without binding resources or mutating playback. Keep their inputs stable for the call. They copy style/pass values and retain geometry, material and any -resolved image bindings. An `CE::Rendering::Submission::ImageParameter2D` inserts the asset image as a draw-layer -sampler using a public contract key and zero-based unit. An empty key, null image or -duplicate draw-layer key throws; omitting it leaves image selection to the other -parameter layers. +resolved image bindings. A default `Context2D` inserts the asset image as the +draw-layer `image` sampler at unit zero. Set `context.image` to an +`ImageParameter2D` for another public key/unit. An empty key, null image or duplicate +draw-layer key throws. Set it to `std::nullopt` to leave image selection to other +parameter layers for sprites, tiles, graphics and legacy fonts. Unicode page +submission requires an image key so each glyph/run selects its matching page. Sprite/tileset cell overloads check logical grid bounds; selected tile and tile-animation values instead rely on their resource range and clip indexing. diff --git a/docs/assets/catalog.md b/docs/site/assets/catalog.md similarity index 66% rename from docs/assets/catalog.md rename to docs/site/assets/catalog.md index d4fa8a4f..b5fdabbb 100644 --- a/docs/assets/catalog.md +++ b/docs/site/assets/catalog.md @@ -2,7 +2,7 @@ The `assets/` Git submodule contains graphics definitions, shaders, UI fixtures and recovered college assets. Initialize its pinned revision with the checkout's -[submodule setup](../../README.md#setup). This catalog lists optional external +[submodule setup](../../../README.md#setup). This catalog lists optional external artwork packages and their image placement; those package PNGs are supplied separately. Definitions live under `graphics/definitions/`, images under `graphics/textures/`. The [manifest guide](asset-manifests.md) owns index selection @@ -12,11 +12,11 @@ and graphics-relative paths. | Package and author page | Download | Tracked manifests | | --- | --- | --- | -| [Mini World Sprites — Shade, octoshrimpy](https://merchant-shade.itch.io/16x16-mini-world-sprites) | `MiniWorldSprites.zip` | [atlas.json](../../assets/graphics/definitions/MiniWorldSprites/atlas.json), [buildings-colored.json](../../assets/graphics/definitions/MiniWorldSprites/buildings-colored.json), [buildings-orc.json](../../assets/graphics/definitions/MiniWorldSprites/Buildings/Enemy/Orc/buildings-orc.json) | -| [MiniWorld Character Customizer — Shade](https://merchant-shade.itch.io/16x16-mini-world-sprites) | `Character-Customizer.zip` | [character-customizer.json](../../assets/graphics/definitions/char-customizer/character-customizer.json) | -| [Puny World — Shade](https://merchant-shade.itch.io/16x16-puny-world) | `punyworld-overworld-tileset.png` or `PUNY_WORLD_v1.zip` | [punyworld-overworld.json](../../assets/graphics/definitions/tilesets/punyworld-overworld.json) | -| [Mage City Arcanos — Hyptosis](https://opengameart.org/content/mage-city-arcanos) | `magecity.png` | [magecity.json](../../assets/graphics/definitions/tilesets/magecity.json) | -| [Dungeon tileset — Buch, with contributions from surt](https://opengameart.org/content/dungeon-tileset) | `dungeon_tiles.png` | [dungeon_tiles.json](../../assets/graphics/definitions/tilesets/dungeon_tiles.json) | +| [Mini World Sprites — Shade, octoshrimpy](https://merchant-shade.itch.io/16x16-mini-world-sprites) | `MiniWorldSprites.zip` | [atlas.json](../../../assets/graphics/definitions/MiniWorldSprites/atlas.json), [buildings-colored.json](../../../assets/graphics/definitions/MiniWorldSprites/buildings-colored.json), [buildings-orc.json](../../../assets/graphics/definitions/MiniWorldSprites/Buildings/Enemy/Orc/buildings-orc.json) | +| [MiniWorld Character Customizer — Shade](https://merchant-shade.itch.io/16x16-mini-world-sprites) | `Character-Customizer.zip` | [character-customizer.json](../../../assets/graphics/definitions/char-customizer/character-customizer.json) | +| [Puny World — Shade](https://merchant-shade.itch.io/16x16-puny-world) | `punyworld-overworld-tileset.png` or `PUNY_WORLD_v1.zip` | [punyworld-overworld.json](../../../assets/graphics/definitions/tilesets/punyworld-overworld.json) | +| [Mage City Arcanos — Hyptosis](https://opengameart.org/content/mage-city-arcanos) | `magecity.png` | [magecity.json](../../../assets/graphics/definitions/tilesets/magecity.json) | +| [Dungeon tileset — Buch, with contributions from surt](https://opengameart.org/content/dungeon-tileset) | `dungeon_tiles.png` | [dungeon_tiles.json](../../../assets/graphics/definitions/tilesets/dungeon_tiles.json) | The three MiniWorld sprite/building manifests share one archive. Character Customizer is a separate archive on the same page. That page also offers `MiniworldGuide.docx` @@ -72,6 +72,46 @@ is 368×384 pixels, with twenty-three columns and twenty-four rows of 16-pixel c This source PNG matches the existing sheet. The separate 0x72 packages below have their own layouts and future manifests. +## Load only installed packages + +The repository's full graphics index references artwork from several packages. +Checking out its manifests alone does not supply those PNGs. After placing one +package, select only its installed definitions instead of calling `load_assets()` +over that entire root. + +For example, after installing Mage City, create +`assets/graphics/selected/graphics-manifests.json`: + +```json +{ + "version": "2.0", + "manifests": ["definitions/tilesets/magecity.json"] +} +``` + +References still resolve from `assets/graphics/`, not the new `selected/` +directory. In platform-owned initialization, with `asset_root` naming `assets` +and `engine` the initialized runtime context: + +```cpp +CE::Assets::Loading::Loader loader(asset_root); +auto prepared = loader.prepare_graphics({"graphics/selected/graphics-manifests.json"}); +loader.upload(std::move(prepared), engine.resources()); +auto tileset = CE::Assets::Tilesets::Manager::get().get_asset("magecity:mage_city"); +if (!tileset) + throw std::runtime_error("The selected Mage City tileset is required"); +``` + +Include ``, +``, `` and ``. +The [loading guide](asset-loading.md#runtime-entry-point) owns the preparation and +upload contract. This selection decodes only images referenced by that index. +Preparation rejects absent images or grids that exceed their decoded dimensions; +check the listed path, filename case and matching artwork revision before retrying. +A nonnull handle confirms the selected identity was published. It is ready for +[tile submission](asset-values-and-playback.md#sample-an-application-map). +For an introductory sprite with included artwork, use [First Game](../getting-started/first-game.md). + ## Packages awaiting manifests These packages need separate manifests for their layouts. Keep original downloads @@ -86,7 +126,7 @@ downloads and provenance records are not included in Git. | [16x16 DungeonTileset II — 0x72](https://0x72.itch.io/dungeontileset-ii) | `dungeontileset-ii/` | `0x72_DungeonTilesetII_v1.7.zip` | Unpacking these packages and writing a manifest for each belongs to the -[long-term asset task](../planning/long-term/README.md#additional-dungeon-asset-manifests). +[long-term asset task](../../planning/long-term/README.md#additional-dungeon-asset-manifests). The extension and remix links on their author pages are separate candidate packages. ## Recovered college assets @@ -98,5 +138,5 @@ under `graphics/fonts/`. The legacy sprite/tile PNGs follow the same ignored loc image policy as external packages; checking out definitions does not supply them. These are recovered project assets, not newly sourced or licensed packages. Their original numeric definitions remain beside the JSON conversions. The submodule's -[provenance notice](../../assets/README.md) still applies where credits or sources +[provenance notice](../../../assets/README.md) still applies where credits or sources are unknown. Conversion alone does not establish native rendering acceptance. diff --git a/docs/assets/file-and-font-discovery.md b/docs/site/assets/file-and-font-discovery.md similarity index 79% rename from docs/assets/file-and-font-discovery.md rename to docs/site/assets/file-and-font-discovery.md index c5e86e58..75ba90dd 100644 --- a/docs/assets/file-and-font-discovery.md +++ b/docs/site/assets/file-and-font-discovery.md @@ -15,7 +15,23 @@ enclosing `graphics/` directory for index, texture and source references. ## File index -[CE::Files::FileIndex](../../projects/engine/include/cheryl/core/resources/fileio/file-mgr.h) +To inspect application files without loading assets, include +`` and ``. With `asset_root` +naming the content directory: + +```cpp +CE::Files::FileIndex files(asset_root); +const auto& pngs = files.get_files_of_type(".png"); +for (const auto& path : pngs) + std::cout << path << '\n'; +``` + +This prints discovered paths; it neither decodes images nor uploads resources. +`pngs` borrows this index. Finish traversal before appending another directory, +then obtain the bucket again if needed. Construct a new `FileIndex` to observe +files added beneath an already scanned directory. + +[CE::Files::FileIndex](../../../projects/engine/include/cheryl/core/resources/fileio/file-mgr.h) is an independent, incremental index. Construction scans the supplied root; `search_directory()` appends an unvisited directory tree. Missing and non-directory roots add nothing. Directories encountered during a scan are marked visited, so @@ -47,6 +63,14 @@ automatic recovery from changed files. ## Font names and application assets +Games configure fonts once through `Context::fonts()` rather than repeating +discovery for text and each GUI adapter. The default is embedded when nothing is +configured. `FontConfiguration` registers stable application references and +retains available style variants; groups contain alternative references for +explicit selection. `FontService::configure(FontSelection)` also preserves an +existing ordered default/fallback policy. See +[catalogue configuration and saved choices](text-layout.md#configure-fonts-once). + [`CE::Text::FontSelection::preferred`](text-layout.md#font-selection) accepts an ordered list of names such as `{"Arial", "Noto Sans", "DejaVuSans.ttf"}`. Family names come from font metadata. Filename aliases, including their extension, @@ -65,9 +89,16 @@ The [text guide](text-layout.md#font-selection) defines ordering, style ranking, failure behavior and the embedded fallback. The lower-level functions below return candidate paths; their filename heuristic is separate from this name resolution. +Configuration retains immutable font bytes. Adapters use those snapshots and +collection indices instead of reopening discovered paths. Missing or unloadable +registered families are unavailable for that catalogue; saved-choice restoration +checks its current group and available members. Ordinary lookup does not rescan +or detect changed files. Reconfigure explicitly when another loading boundary +needs a fresh snapshot; already retained fonts remain valid. + ## System-font candidates -[fonts-system.h](../../projects/engine/include/cheryl/core/resources/fileio/fonts-system.h) +[fonts-system.h](../../../projects/engine/include/cheryl/core/resources/fileio/fonts-system.h) returns owned paths and performs no provider work. `CE::Text::system_font_directories()` lists `/usr/share/fonts`, `/usr/local/share/fonts`, `/Library/Fonts` and `/System/Library/Fonts`, then appends paths derived from nonempty environment values: @@ -106,7 +137,7 @@ inspection and does not try another candidate if later loading fails. ## Legacy font cache -[CE::Text::Legacy::Manager](../../projects/engine/include/cheryl/core/resources/asset-management/font-mgr.h) +[CE::Text::Legacy::Manager](../../../projects/engine/include/cheryl/core/resources/asset-management/font-mgr.h) loads on the active provider's loading owner under the [cache ownership contract](../resources/resource-residency.md#cache-ownership). It uses lexically normalized paths as cache keys, processes the supplied order and retains diff --git a/docs/assets/index.md b/docs/site/assets/index.md similarity index 56% rename from docs/assets/index.md rename to docs/site/assets/index.md index 0124f38c..4773fa36 100644 --- a/docs/assets/index.md +++ b/docs/site/assets/index.md @@ -1,14 +1,21 @@ -# Assets +(assets)= +# Assets and animation Load application content, author graphics manifests, and keep animation or tile selection in game-owned state. The [Assets system](../engine-systems/assets.md) -introduces the public types; the sprite and animation tasks cover first use. +introduces the public types. [First Game's sprite walkthrough](../getting-started/first-game.md#put-a-sprite-on-screen) +covers loading, placement and submission; [Play sprite animation](../getting-started/how-to/animate-sprite.md) +adds a per-actor playback cursor. +For a background PNG without a sprite definition, use the +[whole-image path](../getting-started/how-to/draw-sprite.md#draw-a-whole-image). ```{toctree} :maxdepth: 1 +:titlesonly: Load and publish assets Author graphics manifests +Play sprite animation <../getting-started/how-to/animate-sprite> Animate sprites and select tiles Discover files and font candidates Install optional artwork packages diff --git a/docs/assets/text-encoding.md b/docs/site/assets/text-encoding.md similarity index 97% rename from docs/assets/text-encoding.md rename to docs/site/assets/text-encoding.md index 98fa422a..140c6d6b 100644 --- a/docs/assets/text-encoding.md +++ b/docs/site/assets/text-encoding.md @@ -1,5 +1,7 @@ # Text encoding and source indices +Include `` for the decoder and its owned scalar records. + `CE::Text::decode_utf8_scalar(text, offset)` reads one scalar from a complete UTF-8 view. `CE::Text::decode_utf8(text)` returns owned `CE::Text::Utf8Scalar` records for the entire view. Each record stores the scalar, original byte offset/count and a `replaced` flag; diff --git a/docs/assets/text-layout.md b/docs/site/assets/text-layout.md similarity index 56% rename from docs/assets/text-layout.md rename to docs/site/assets/text-layout.md index d7e1c4c6..76d33bcb 100644 --- a/docs/assets/text-layout.md +++ b/docs/site/assets/text-layout.md @@ -4,8 +4,8 @@ | Content | Use | Working sequence | | --- | --- | --- | -| A game-owned label or HUD message | Engine font selection, shaped layout and retained text generations | [Display and update text](../getting-started/how-to/update-text.md): prepare on CPU, upload on platform, adopt a ready result and submit it in a frame. | -| Text inside a native widget or RML document | The selected toolkit's authoring and font interfaces | The UI session owns layout/editing and publishes a retained scene; Engine supplies CPU font services underneath. | +| A game-owned label or HUD message | Engine's configured default or a registered font, retained text updater | [Display and update text](../getting-started/how-to/update-text.md): supply content/options, advance replacement, and submit its current generation in a frame. | +| Text inside a native widget or RML document | The selected toolkit's authoring interfaces with Engine fonts | A Context-aware UI session installs Engine's default. Use the [TGUI](../modules/tgui.md#fonts) or [RmlUi](../modules/rmlui.md#fonts) bridge to select a registered font for an element. | | Existing ASCII or bitmap-atlas consumers | The legacy Font interfaces and their metrics | Keep their atlas, scale and bank compatibility contract during migration. | The [Text system](../engine-systems/text.md) identifies the public types, while @@ -18,9 +18,9 @@ The initial rendering scope is accented Latin (including English, French and Ger Russian/Cyrillic, mixed paragraph direction and optional width-constrained wrapping. Glyph coverage and shaping are font/script concerns rather than a separate renderer for every language. Color emoji is a planned -[next text batch](../planning/mid-term/README.md#color-emoji); +[next text batch](../../planning/mid-term/README.md#color-emoji); wider CJK/script acceptance remains in the -[long-term plan](../planning/long-term/README.md#other-engine-extensions). +[long-term plan](../../planning/long-term/README.md#other-engine-extensions). Display source maps do not establish caret movement, selection or IME/preedit. The service pairs face-qualified glyph placements with complete immutable page @@ -37,7 +37,7 @@ that fallback model. Its `AsciiFont`/`BitmapFont` implementations and toolkit-ow text retain their separate roles. Engine also owns the opaque -[`CE::Text::FontRasterizer`](../../projects/engine/include/cheryl/text/font-rasterizer.h) +[`CE::Text::FontRasterizer`](../../../projects/engine/include/cheryl/text/font-rasterizer.h) CPU service for loading copied font bytes, properties, metrics, glyph pixels and kerning. Copies retain and serialize native face state; returned data owns its storage and needs no provider/context. TGUI and RmlUi adapters use this shared @@ -52,6 +52,159 @@ the same Cheryl text interfaces on both platforms; see [dependency settings](../development/building.md#dependency-and-toolchain-settings) for build and runtime requirements. +## Configure fonts once + +Include `` for catalogue configuration and retained +fonts, and `` when laying out direct Engine text. + +`engine.fonts()` owns a `CE::Text::FontService`. With no configuration, +`default_font()` returns Engine's embedded DejaVu Sans face; displaying a label +does not require a font file or installed system font: + +```cpp +auto font = engine.fonts().default_font(); +auto layout = CE::Text::layout_text(font, "Score: 0", {.pixel_height = 24}); +``` + +Configure application fonts at startup, before creating UI sessions or other +font consumers. A `FontFamily` gives a font a stable application reference. +Its sources supply the family's regular, bold and italic faces. A named group +contains alternative family references to choose from; it is separate from +style variants and glyph fallback. + +```cpp +CE::Text::FontConfiguration configuration; +configuration.asset_directories = {asset_root}; +configuration.families = { + {"interface", {"Interface.ttf"}}, + {"book-serif", {"Book Serif"}}, + {"handwriting", {"Handwriting.ttf"}} +}; +configuration.default_font = "interface"; +configuration.groups = {{"books", {"book-serif", "handwriting"}}}; +configuration.fallbacks = {"interface"}; +engine.fonts().configure(configuration); +``` + +The filenames and family names above are application choices. Sources use the +same `FontSource` forms and discovery roots as [Font selection](#font-selection). +Named-family registration retains available style variants, including explicit +collection-face indices. Explicit files can list separate regular/bold/italic +sources under one reference. Registering a specialty font does not add it to +other fonts' fallback chains; `fallbacks` selects that common chain explicitly. +The embedded face remains its terminal fallback. +Adapters obtain the complete ordered chain through `FontService::fallbacks()`; +each returned handle contributes its primary family, including when the default +family also appears in the configured fallback list. + +`default_font()` returns the configured default when available, otherwise the +embedded font. `get("handwriting")` selects that registered font; an unknown or +unavailable reference resolves to the default. `all()` lists the available +registered handles and the embedded handle. References are case-sensitive +application identifiers, independent of registration order and physical family +names. Empty references, embedded NUL and the reserved `cheryl:embedded` reference +are invalid for application registrations. Duplicate family references reject +configuration. + +Context-aware TGUI and RmlUi sessions obtain the configured fonts directly. +Their default inherits through the toolkit's widgets/documents. For a specific +element, use TGUI's `font()`/`set_font()` or RmlUi's `set_font()` bridge with an Engine +`Font`, or the RmlUi alias returned by `font_family()`. Native widget authoring remains in +the selected toolkit; see [GUI integration](../engine-systems/gui.md) for ownership +and the [TGUI](../modules/tgui.md#fonts) or [RmlUi font contract](../modules/rmlui.md#fonts) +for native style and alias rules. + +### Retained font handles and style + +`CE::Text::Font` is a copyable, resolved CPU handle. It retains font bytes, style +metadata, collection indices and its ordered fallback families. It remains usable +after the service or Context is destroyed. `layout_text(font, text, options)` +does not need a current Engine context. Select a style through layout options: + +```cpp +auto book_font = engine.fonts().get("book-serif"); +auto heading = CE::Text::layout_text( + book_font, "Chapter One", + {.pixel_height = 32, .font_style = {.weight = 700, .italic = false}} +); +``` + +`font.collection(style)` selects the closest available style from each retained +family for the existing collection-based APIs. Selection prefers matching italic +state, then the closest numeric weight. Adapter-facing `faces()` exposes borrowed +bytes and face/style metadata; retain the `Font` while using those views. +`FontRasterizer::load(font, style)` opens the selected primary style, while +`load(face)` opens an exposed face directly. A single rasterizer does not perform +the collection's fallback selection. + +### Choose and save a font + +Random choice is explicit. `choose_random("books")` returns a resolved `Font`; +ordinary font lookup and rendering do not choose again. To restore a saved choice, +pass its reference to `restore_or_choose()`: + +```cpp +// These two save functions belong to the application. +const std::string saved_reference = load_book_font(book_definition_id); +auto book_font = engine.fonts().restore_or_choose("books", saved_reference); +save_book_font(book_definition_id, book_font.reference()); + +auto page = CE::Text::layout_text(book_font, contents, {.pixel_height = 24}); +``` + +Run this at the application's shared book initialization boundary. Keying saved +choices by book definition makes other copies of the same book reuse the choice; +an instance key permits individually styled copies. The book keeps the returned +font handle. The application owns these identities and its save format. + +Restoration keeps a reference only while it is still in the selected group and +its font loaded successfully. Otherwise it chooses among the group's available +members. A missing/empty group or one with no available members uses the +application default, then the embedded fallback. Save the returned `reference()` +to persist replacements. Group changes do not reroll an existing choice that +remains available and belongs to the group. + +The simple overload uses the service's random generator. Overloads taking a +caller-owned `std::mt19937_64&` support controlled selection; restoring an available +group member does not advance it. Save the chosen reference, not the random seed, +runtime handle, or font bytes. Changing group contents or random call order cannot +reinterpret an already saved reference. + +### Loading and availability + +Configuration loads CPU snapshots and publishes the complete catalogue together. +Missing or unloadable optional families are unavailable for this configuration; +invalid source syntax and allocation failures propagate. Failed configuration +leaves the previous catalogue usable. Catalogue operations are synchronized, +and existing font handles remain valid when configuration is replaced. Configure +before consumers: replacement does not update existing widgets or font handles. + +Lookup does not rescan files or detect source replacement. Removing a source file +after configuration does not invalidate its retained bytes. A later explicit +configuration evaluates availability again. References identify the application's +font choices; they do not identify a particular revision of a font file. + +For an application already using an ordered `FontSelection`, preserve that policy +without extracting font paths for each toolkit: + +```cpp +CE::Text::FontSelection selection; +selection.preferred = {"Arial", "Noto Sans", "DejaVuSans.ttf"}; +selection.asset_directories = {asset_root}; +engine.fonts().configure(selection); +auto font = engine.fonts().default_font(); +``` + +This overload retains the existing selection and fallback order under the stable +reference `application-default`. It defines the default without named random +groups. Retained source faces are also registered by path and collection index, +independently of preference-list positions; embedded always uses `cheryl:embedded`. +Use `configure(selection, "demo")` to name the ordered default policy when native +documents already refer to that family; the name cannot collide with embedded or +selected source references. +The standalone collection interface below remains available for explicit +CPU font processing. + ## Font selection `CE::Text::FontCollection::load(selection)` creates an immutable CPU snapshot of font @@ -117,7 +270,7 @@ names, files and families active. Preferred sources remain unrestricted by this limit; name/family lookup can select a heavy face when no lighter one exists. These preferences are not guaranteed to exist across operating systems or regions. Repeated path/face entries are deduplicated. The terminal fallback is always an -[embedded, licensed DejaVu Sans](../../assets/fonts/README.md) face independent of the +[embedded, licensed DejaVu Sans](../../../assets/fonts/README.md) face independent of the application's fonts, UI modules and installed inventory. The fallback covers the initial alphabets and contains a visible U+FFFD replacement; it cannot supply the original glyph for every Unicode character. @@ -196,7 +349,11 @@ font provider's row/UV order. The configured maximum page extent is 4..4096 pixe There is no shared glyph cache, in-place repack, eviction or residency budget. `CE::Text::Rendering::upload(prepared, provider)` copies CPU buffers into fresh immutable page -geometry/atlas handles on the provider's upload owner. Passing a copy preserves CPU +geometry/atlas handles on the provider's upload owner. A nonempty generation also +retains the provider's default textured-triangle material; an optional third +material argument selects an explicit handle instead. No application shader +files or material factory are required for the default path. Empty text needs no +built-in material capability. Passing a copy preserves CPU preparation for retry; moving it into an owned [dispatcher request](../runtime/thread-dispatch.md) transfers that value. A successful call returns one `CE::Text::Rendering::Generation` with its matching @@ -205,15 +362,51 @@ Already created transient resources follow backend retirement; this is not an at native-resource transaction. Callers publish a complete candidate only after success. `CE::Rendering::Submission::resolve_text(rendered, style, context)` performs CPU packet resolution alongside -the legacy font overload. Supply a nonempty `context.image` pipeline key/unit so -each glyph selects its own generation page; that key must be absent from draw -parameters. Glyph offsets are scaled and transformed by the supplied model exactly +the legacy font overload. An absent `style.material` uses the generation's retained +material. A default `Context2D` supplies the public `image` key at unit zero so +each glyph selects its own generation page. Custom contexts can choose another +nonempty key/unit; that key must be absent from draw parameters. Glyph offsets are scaled and transformed by the supplied model exactly as in legacy submission. Packets copy values and retain both page handles. Replacing or releasing the text/font/preparation cannot pair old quad ranges with a newer atlas. Old submitted frames retain their resources, subject to the original backend domain remaining alive for native use. Submission performs no shaping, discovery, rasterization, upload, binding or drawing. +### Retained text updates + +`CE::Text::Rendering::Updater` in `` owns the +replacement sequence for a changing label. Construct it with a `Context` during +platform initialization, then call `request()` and nonblocking `update()` on the +simulation owner. The application supplies a value-owned `Request`: font, UTF-8 +text, layout/raster options and optional material. An empty font selects the +default captured at construction. A synchronous initial-request constructor +provides `current()` immediately; the constructor without a request starts empty. + +Preparation uses a context-tracked worker group; upload uses its saved platform +submission endpoint. Jobs retain owned values and never borrow the updater or +game. Runtime's existing worker/dispatcher shutdown remains responsible for +settling or cancelling accepted work. Destroying the updater does not wait for +jobs. Serialize updater calls and stop simulation use before platform cleanup. + +One active request and the newest desired request bound outstanding work. +Equal requests compare all layout/raster fields, material identity and retained +font-snapshot identity; they submit no duplicate work. A completed active result +is adopted even if a newer request is waiting, then that newest request starts. +Intermediate desired requests are coalesced. Older frames retain their resources. + +Asynchronous submission/execution failures leave the previous `current()` intact. +`failure()` retains the preparation/upload stage, exception and failed revision +for application reporting. A distinct request clears that report; retrying a +settled failed request clears it too. An older active request can subsequently +fail while a newer desired request proceeds. Unchanged failed work stays settled +until explicit retry. +Initial synchronous construction failures propagate. No automatic retry loop or +background mutation of the game's current generation occurs. + +The [text task](../getting-started/how-to/update-text.md) gives the complete +initialization/update/submission/cleanup sequence. Direct `layout_text()`, +`prepare()` and `upload()` remain available for application-owned scheduling. + ### Positioned text runs Set `CE::Text::Rendering::RasterOptions::positioned_runs = true` during `CE::Text::Rendering::prepare()` to retain @@ -229,7 +422,7 @@ The image key/unit, immutable sampling, clipping, material and whole-text model/ remain those selected by the caller. Packets retain the positioned geometry and matching atlas generation through replacement/release; empty text emits no draws. -This overload requires a shader applying the common scale/model transform to the +The default material supports this overload. Custom materials require a shader applying the common scale/model transform to the positioned vertices, without per-glyph or draw-identity semantics. The API cannot infer that contract from arbitrary shaders. Use `CE::Rendering::Submission::resolve_text()` for those shaders or when the original per-glyph raster evaluation is required: regrouping floating-point @@ -237,11 +430,10 @@ transforms can change coverage of exact boundary pixels. The default preparation and submission remain per glyph. This facility does not merge arbitrary frame packets, reorder draws or change either UI adapter's batching policy. -The [demo](../../projects/apps/demo/README.md#builtin-unicode-text) submits immutable -HUD/camera text, prepares replacements on a CPU worker and uploads through the -platform dispatcher. Its preview supports builtin-only, automatic, explicit file/ +The [demo](../../../projects/apps/demo/README.md#builtin-unicode-text) submits immutable +HUD/camera text and uses the retained updater for changing HUD content. Its preview supports builtin-only, automatic, explicit file/ family and paragraph-direction choices. The demo guide preserves -[native preview procedures](../../projects/apps/demo/README.md#unicode-preview-validation). +[native preview procedures](../../../projects/apps/demo/README.md#unicode-preview-validation). Its `--text-runs` option selects positioned submission for builtin text and sample labels; toolkit-owned text keeps its separate path. @@ -253,7 +445,7 @@ scope. Similar output across runtime/font variants is compatible with the same effective glyph faces and placements. Right-aligned Latin under explicit RTL and unchanged Hebrew-only output between automatic/RTL modes match the direction contract above; this native report does not independently confirm Hebrew glyph order. -The accepted [CPU bidi case](../../projects/engine/tests/unit/src/text-layout.cpp) +The accepted [CPU bidi case](../../../projects/engine/tests/unit/src/text-layout.cpp) checks Hebrew/mixed visual source order and preserves Latin order under explicit RTL. Alignment alone is not proof of every bidi sequence. @@ -264,8 +456,10 @@ and replacement lifetime. It does not capture the desktop or other applications. These fixture comparisons and CPU submission measurements do not establish every boundary-pixel identity, physical-GPU timing or other-platform appearance. -Dedicated numeric font-weight/style fixtures remain in the -[Unicode plan](../planning/short-term/unicode-text.md#progress). Color/CJK rendering, +CPU and adapter fixtures cover registered weight/style selection and collection +face indices. Automatic discovery's numeric-weight boundaries remain in the +[font-selection plan](../../planning/short-term/README.md#font-selection-regressions). These fixtures do +not establish native visual correctness of distinct authored style faces. Color/CJK rendering, grapheme/bidi editing, IME and additional platforms retain separate scope. Reusable [CPU checks](../development/architecture-validation.md#engine-asset-and-text-regressions) and these Linux observations do not establish other-platform coverage. diff --git a/docs/audio/index.md b/docs/site/audio/index.md similarity index 89% rename from docs/audio/index.md rename to docs/site/audio/index.md index 4d19d200..a455c070 100644 --- a/docs/audio/index.md +++ b/docs/site/audio/index.md @@ -6,8 +6,10 @@ lifetime. The [Audio system](../engine-systems/audio.md) introduces the types. ```{toctree} :maxdepth: 1 +:titlesonly: Control playback and ownership <../runtime/audio> +miniaudio reference <../modules/miniaudio> ``` The [miniaudio module](../modules/miniaudio.md#preparation-and-output) supplies the diff --git a/docs/development/architecture-validation.md b/docs/site/development/architecture-validation.md similarity index 85% rename from docs/development/architecture-validation.md rename to docs/site/development/architecture-validation.md index 7144b829..3b92bf8d 100644 --- a/docs/development/architecture-validation.md +++ b/docs/site/development/architecture-validation.md @@ -1,11 +1,32 @@ -# Architecture validation +(architecture-validation)= +# Component and SDK checks This guide describes reusable checks for the selected Engine, Native GLFW and -OpenGL assembly. [The module guide](modules.md) defines target ownership, public -usage requirements, selection options and test runners. Use these procedures when +OpenGL assembly. [Module authoring](modules.md) defines target boundaries and +independent owners; [Source and SDK integration](consuming-engine.md) owns application +selection, public usage requirements and deployment. Use these procedures when changes affect isolation, composition or consumer contracts; coverage limits remain separate from source completion. +## Start with the changed boundary + +Use [Testing and validation](testing.md#choose-checks-for-a-change) for runner +selection. This page owns the deeper composition and acceptance procedures: + +| Change | Procedure | +| --- | --- | +| Exported header, public target or standalone owner | [Independent consumers](#independent-consumers-and-header-probes) and [component configurations](#configure-component-combinations). | +| SDK export, relocation or native dependency packaging | [Installed SDK consumers](#installed-sdk-consumers). | +| Asset/text replacement or partial-publication behavior | [Engine asset and text regressions](#engine-asset-and-text-regressions). | +| Worker placement, JSON or topology | [Worker topology and configuration](#worker-topology-and-configuration). | +| Typed event delivery and closure | [Typed events](#typed-events). | +| Native rendering, sampling or retained text | [Sampling and positioned text](#sampling-and-positioned-text), with [native/fixture prerequisites](#native-and-fixture-checks). | +| Logging/signal driver behavior | [Manual acceptance drivers](#manual-acceptance-drivers). | + +Start with the relevant procedure and expand only when the changed contract +crosses another boundary. [Evidence limits](#evidence-limits) distinguish controlled +checks from native execution and physical observation. + ## Coverage and selection | Selection | Required checks | @@ -20,13 +41,55 @@ separate from source completion. | Installed SDK | Extract into another prefix, reject producer-path leakage, compile public/Startup/native-toolkit probes and link only installed owners. Check packaged and system shared runtimes with both Cheryl linkages. Static consumers also resolve embedded archives and native final-link requirements. Source-tree consumers alone do not prove relocation. | Controlled Engine tests establish runtime, publication and failure behavior against -predictable dependencies. Implementing modules own checks of their actual behavior +predictable dependencies. In-memory display/window, input, presentation, renderer +and resource-provider adapters exercise both runtime modes without OpenGL/GLFW +headers. Implementing modules own checks of their actual behavior and contract compliance; passing with a dummy does not establish module conformance. Broader Engine cases cover dispatch/cancellation, events, workers, timing, caches, retained frames and failure paths. Owner runners and the combined runner reuse case objects, so choose one runner or CTest prefix for each scenario rather than executing the same cases through every aggregate. +## Independent consumers and header probes + +[Engine consumer](../../../projects/engine/tests/consumer/CMakeLists.txt) 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](../../../projects/modules/platform/native-glfw/tests/consumer/CMakeLists.txt) +and [OpenGL consumer](../../../projects/modules/graphics/opengl/tests/consumer/CMakeLists.txt) +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: + +```sh +( + 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 locate their enclosing checkout automatically; +`CHERYL_REPOSITORY_ROOT=/path/to/cheryl-engine` explicitly selects another checkout +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 [Module authoring](modules.md). The procedures below cover independent graphs, +selected runners and native acceptance; source consumers alone do not establish +installed SDK relocation. + ## Repeating validation Use the toolchain and selected-owner prerequisites in the [build guide](building.md). @@ -178,9 +241,9 @@ Engine-only selection; startup support and the independent consumer also compile The existing `font_selection.families` fixture establishes basic discovery and ordering; it does not cover declared-weight ranking or heavy-only automatic exclusion. Those fixture/case gaps remain in the -[Unicode plan](../planning/short-term/unicode-text.md#progress). Startup and UI +[font-selection plan](../../planning/short-term/README.md#font-selection-regressions). Startup and UI callback/placeholder regression gaps are in the -[roadmap checklist](../planning/develop-review-and-development-plan.md#startup-and-ui-follow-up). +[startup/UI checklist](../../planning/short-term/README.md#startup-and-ui-follow-up). Graphics 2.0, indexed shader/sampling and positioned-text CPU checks are accepted in the selected Linux compositions. The @@ -271,7 +334,7 @@ The Linux producer uses distribution development packages. Windows CI uses the same steps under its MSVC Developer PowerShell with the prepared shared dependency toolchain. Platform acceptance remains subject to the [CI coverage limits](ci.md). The first main-push -artifact collection remains [TR20](../testing-requests.md#tr20-automated-main-push-sdk-collection). +artifact collection remains [TR20](../../testing-requests.md#tr20-automated-main-push-sdk-collection). This reusable procedure does not assert acceptance of unexecuted new cases. ### Typed events @@ -292,9 +355,9 @@ Run the focused unit and acceptance selections: ) ``` -The [unit cases](../../projects/engine/tests/unit/src/typed-events.cpp) cover exact +The [unit cases](../../../projects/engine/tests/unit/src/typed-events.cpp) cover exact identity, type constraints, owned payloads, removal, nested dispatch and immediate -errors. The [acceptance cases](../../projects/engine/tests/acceptance/src/typed-events.cpp) +errors. The [acceptance cases](../../../projects/engine/tests/acceptance/src/typed-events.cpp) cover deferred copies, cancellation, error-sink ownership, waits and worker FIFO. `event_delivery.runtime_owner_threads` exercises typed resize payloads on platform and simulation owners in sequential and concurrent runtime modes without borrowing @@ -316,7 +379,7 @@ These cases invoke the actual registered C callback to check unchanged-size suppression, saved observations across nested resize delivery, legacy-before-typed offers and native failure consumption. They do not establish compositor-generated resize delivery. Skipped native cases leave that callback coverage pending. Windows execution -is shelved in the [platform plan](../planning/long-term/platform-acceptance.md). +is shelved in the [platform plan](../../planning/long-term/platform-acceptance.md). ### Native, OpenGL and standalone composition @@ -464,7 +527,7 @@ Native OpenGL cases require `CHERYL_NATIVE_GL_TESTS=1` and a usable display: ) ``` -[Native cases](../../projects/modules/graphics/opengl/tests/acceptance/src/native-opengl.cpp) +[Native cases](../../../projects/modules/graphics/opengl/tests/acceptance/src/native-opengl.cpp) cover resource retirement, context restoration, retained-frame reload, failed construction/reflection cleanup, texture row conventions, timing/backpressure and composed runtime failure cleanup. Include the replacement-poll/platform-drain @@ -480,7 +543,7 @@ If current demo evidence is unavailable, run finite sequential/concurrent sessio with `--max-updates`; interactive checks are in [native-desktop-checks.md](native-desktop-checks.md). -[Real-font allocation checks](../../projects/engine/tests/all-tests/src/resources/fonts.cpp) +[Real-font allocation checks](../../../projects/engine/tests/all-tests/src/resources/fonts.cpp) need `CHERYL_STB_ALLOCATION_TTF` and `CHERYL_STB_ALLOCATION_CFF` to name suitable TrueType and CFF OpenType faces, each with a glyph wider than 64 pixels at size 128. DejaVuSans.ttf and NimbusSans-Regular.otf are example fixtures. Export both paths @@ -489,7 +552,7 @@ combined runner. These cases sweep bake scratch allocation failures and nested scopes, requiring released scratch and no provider upload on failure. Missing fixture paths skip; unusable supplied paths fail. -[Linux worker checks](../../projects/engine/tests/all-tests/src/core/worker-pool.cpp) +[Linux worker checks](../../../projects/engine/tests/all-tests/src/core/worker-pool.cpp) exercise inherited-mask discovery/restoration and native kernel rejection. They can skip when required CPUs/capabilities are unavailable; controlled policy/thread-start faults provide different coverage. Identify configuration, opt-ins and skips when @@ -537,5 +600,5 @@ Sanitizers supplement selected checks; TSan requires a configuration without the Debug ASan/UBSan combination and cannot be inferred from those runs. Keep source completion distinct from executable acceptance and unavailable-host -coverage. Unresolved work remains in the [planning catalogue](../planning/README.md); +coverage. Unresolved work remains in the [planning catalogue](../../planning/README.md); this guide is a reusable procedure, not an execution journal. diff --git a/docs/development/building.md b/docs/site/development/building.md similarity index 81% rename from docs/development/building.md rename to docs/site/development/building.md index 21a3517b..c119f054 100644 --- a/docs/development/building.md +++ b/docs/site/development/building.md @@ -1,8 +1,9 @@ -# Configuring and building Cheryl +(configuring-and-building-cheryl)= +# Build engine and SDKs Use CMake 3.28 or newer, Ninja and a C++23 compiler/standard library with `std::format`. Initialize the pinned submodules as described in -[setup](../../README.md#setup). Build-tree consumers use the same dependencies; +[setup](../../../README.md#setup). Build-tree consumers use the same dependencies; link Cheryl targets instead of copying include and link settings. Prebuilt consumers use the [SDK import targets](consuming-engine.md#prebuilt-development-sdks). @@ -15,7 +16,7 @@ source directories. SDK installation collects them into one redistributable | Artifact | Trigger | Result | | --- | --- | --- | -| HTML manual and C++ reference | Run `./generate-docs.sh`, or build `docs` in the docs-only project below; `--clean` / `docs-clean` forces full regeneration. Enable `CHERYL_BUILD_DOCS` to add these targets to an engine build. | `docs/generation/generated/html/index.html` and linked pages. | +| HTML manual and C++ reference | Follow [Documentation tooling](documentation.md) for direct generation or the CMake documentation targets. | `docs/site/generation/generated/html/index.html` and linked pages. | | Engine library | Build `cengine`; see [engine-only configuration](#engine-only-configuration). | Versioned Engine library in the build root, plus required dependency artifacts; see [library linkage](#library-linkage). | | Selected SDK libraries | Enable `CHERYL_BUILD_SDK`, then build `cheryl-sdk`. | Engine, selected modules/support and their owned dependencies in the build tree. | | Redistributable SDK directory | Build `cheryl-sdk`, then install the `CherylSDK` component. | One prefix containing `include/`, libraries, CMake package files and licenses. | @@ -28,38 +29,23 @@ dependencies for ``. Leave `CHERYL_BUILD_SDK=OFF` when building application from source unless you also want to produce an SDK. This interface target links the existing libraries; there is no additional combined library archive. See -[consuming Cheryl](consuming-engine.md) for source and prebuilt integration. +[Source and SDK integration](consuming-engine.md) for source and prebuilt integration. ### Build documentation -Run `./generate-docs.sh` from the checkout root for direct generation, or add -`--clean` for full regeneration. The documentation-only CMake project provides -the same operations through `docs` and `docs-clean`. Neither path needs a C/C++ -compiler or engine dependencies. Run this block from any directory inside the -checkout; the subshell preserves your starting directory: - -```sh -( - set -e - cd "$(git rev-parse --show-toplevel)" - cmake -S docs/generation -B docs/generation/generated/cmake -G Ninja - cmake --build docs/generation/generated/cmake --target docs -) -``` - -Open `docs/generation/generated/html/index.html`. Python 3.11 or newer is required; -the generator selects compatible installed tools or prepares pinned tools in the -user cache. Initial tool setup may need network access. See -[documentation generation](documentation.md) for shell/Python entry points, tool -selection and output options. The `docs` and `docs-clean` targets prepare their -tools automatically. +[Documentation tooling](documentation.md) owns generation commands, prerequisites, +tool selection and output options. Its [CMake entry points](documentation.md#cmake-entry-points) +provide `docs` for incremental generation and `docs-clean` for full regeneration +without compiling the engine. Enable `CHERYL_BUILD_DOCS` to expose these targets +in an engine build, or use the documentation-only project to avoid engine +configuration and dependencies. ## Configuration options Pass settings as `-DNAME=value`. These defaults apply to a fresh root configuration; existing build directories and IDE profiles retain their cache. All Cheryl-owned option definitions and defaults live in -[`cmake/CherylOptions.cmake`](../../cmake/CherylOptions.cmake). Component and +[`cmake/CherylOptions.cmake`](../../../cmake/CherylOptions.cmake). Component and standalone entry points select which options to declare; optional backend and dependency-source settings may appear only when their component is selected. @@ -78,7 +64,7 @@ dependency-source settings may appear only when their component is selected. | `CHERYL_BUILD_AUDIO_MINIAUDIO` | `ON` | Selects miniaudio decoding, output and streaming. | | `CHERYL_BUILD_DEMO` | `OFF` | Adds `demo` with Native GLFW, OpenGL and native input selected. | | `CHERYL_BUILD_FIRST_GAME` | `OFF` | Adds the maintained `first-game` walkthrough application with Native GLFW, OpenGL and native input selected. | -| `CHERYL_BUILD_TUTORIAL_GAME` | `OFF` | Adds `tutorial-game` with Player/Enemy, a direct text score and TGUI controls; requires Startup/OpenGL, native input and TGUI. | +| `CHERYL_BUILD_TUTORIAL_GAME` | `OFF` | Adds `tutorial-game` with reusable controllers, Characters, player join/leave, patrol and a TGUI HUD; requires Startup/OpenGL, native input and TGUI. | | `CHERYL_BUILD_DOCS` | `ON` | Adds the optional `docs-tools`, `docs` and `docs-clean` targets; tools are selected when these targets run. | | `CHERYL_BUILD_TESTS` | `OFF` | Adds focused tests and explicitly buildable acceptance/aggregate runners. | | `CHERYL_BUILD_ALL_TESTS` | `OFF` | Includes aggregates in the default build and CTest; requires tests. | @@ -127,8 +113,8 @@ Executables and archives use the configured build root. Windows adds `.exe` to executables. Multi-configuration generators require `--config Release` for builds and `-C Release` for CTest, with executables normally in `Release/`. Target and artifact declarations are centralized in -[CherylTargets.cmake](../../cmake/CherylTargets.cmake) and -[CherylOutputs.cmake](../../cmake/CherylOutputs.cmake). +[CherylTargets.cmake](../../../cmake/CherylTargets.cmake) and +[CherylOutputs.cmake](../../../cmake/CherylOutputs.cmake). ### Library linkage @@ -143,7 +129,7 @@ binary. The Linux terminal's `main` wrapper and argument/startup attachment rema static; its viewer is an executable. Artifact names use the component versions in -[CherylVersions.cmake](../../cmake/CherylVersions.cmake). For Engine 4.0.0: +[CherylVersions.cmake](../../../cmake/CherylVersions.cmake). For Engine 4.0.0: | Platform | Static | Shared | | --- | --- | --- | @@ -184,15 +170,15 @@ satisfy the same policy; incompatible targets fail configuration. | Library | Linkage | Ownership and deployment | | --- | --- | --- | -| FreeType, HarfBuzz | Shared | Private Engine implementation. Use Linux development packages or prepared Windows DLLs/import libraries. | -| ICU | System shared | Linux uses the distribution's `uc`, `i18n` and `data` libraries. Windows uses the OS C API from `icu.dll`; Windows 10 version 1903 or later and a Windows SDK containing `icu.h`/`icu.lib` are required. | +| FreeType, HarfBuzz | Shared | Private Engine implementation. Source builds use Linux development packages or prepared Windows DLLs/import libraries; both SDK linkages package the selected native runtimes/imports. | +| ICU | Shared; system-provided on Windows | Linux source builds use `uc`, `i18n` and `data`; SDKs package those selected libraries. Windows uses the OS C API from `icu.dll`; Windows 10 version 1903 or later and a Windows SDK containing `icu.h`/`icu.lib` are required. | | GLFW | Shared | Native GLFW owns its integration; the owned source build supplies one shared GLFW runtime. | | HIDAPI | Shared when HID is enabled | Reuse a compatible package or build the pinned source. Gainput remains embedded in Native GLFW. | | TGUI, RmlUi Core | Shared | Reviewed custom builds expose native authoring APIs. Private callbacks reach Engine services through their Cheryl adapters; the toolkit libraries do not link Engine. | | hwloc 2.5+ (2.x) | Shared when selected | Private Workers implementation; disable `CHERYL_BUILD_WORKER_TOPOLOGY_HWLOC` to omit discovery and linkage. | | OpenGL and native platform libraries | System shared | Use the host's graphics driver and OS libraries. Generated GLAD is embedded in the Cheryl OpenGL owner. | | spdlog, Gainput, miniaudio, GLAD, stb | Embedded | Each compiled implementation has one Cheryl owner; shared owners require position-independent archive inputs. | -| Backward support | Embedded headers/bootstrap, system shared diagnostics | Engine owns crash initialization. Selected Linux libdw/libelf and compression libraries or Windows dbghelp/psapi remain platform dependencies. | +| Backward support | Embedded headers/bootstrap, selected shared diagnostics | Engine owns crash initialization. SDKs package selected non-system Linux diagnostic/compression dependencies; Windows dbghelp/psapi remain OS requirements. | | GLM, CTTI, CLI11 | Headers/templates | Public target usage requirements remain explicit; CLI11 belongs to Startup. JSON stays private to its consuming implementations. | Microsoft documents the [Windows ICU C API and combined 1903+ runtime](https://learn.microsoft.com/en-us/windows/win32/intl/international-components-for-unicode--icu-). @@ -244,20 +230,20 @@ Use a matching prepared target triplet for other architectures and cross builds. | Setting | Purpose | | --- | --- | | `GLFW_BUILD_WAYLAND=OFF` | Uses bundled GLFW's X11 backend on Linux without Wayland development tools. | -| `GAINPUT_ENABLE_HID=OFF` | Omits Gainput's HID support and HIDAPI discovery/fetch on Linux; this is the Linux default. Windows forces that backend and uses shared HIDAPI. Use the [current Linux joystick path](../../projects/modules/platform/native-glfw/README.md#controller-backend-limits) while HID integration remains unresolved. | +| `GAINPUT_ENABLE_HID=OFF` | Omits Gainput's HID support and HIDAPI discovery/fetch on Linux; this is the Linux default. Windows forces that backend and uses shared HIDAPI. Use the [current Linux joystick path](../modules/native-glfw.md#controller-backend-limits) while HID integration remains unresolved. | | `Python_EXECUTABLE=/path/to/python` | Selects GLAD's interpreter; Jinja2 must be installed in it. | | `CMAKE_POLICY_VERSION_MINIMUM=3.5` | Allows pinned legacy dependency files to configure with CMake 4 when needed. | | `CHERYL_REPOSITORY_ROOT=/path/to/cheryl-engine` | Explicitly selects the checkout providing shared helpers and Engine bootstrapping for standalone modules/consumers; entry points otherwise locate their enclosing checkout. | | `CHERYL_NATIVE_GLFW_SOURCE=/path/to/native-glfw` | Supplies Native GLFW to standalone OpenGL when its target is absent. | | `CHERYL_TGUI_SOURCE`, `TGUI_DIR` | Selects a TGUI 1.13.0 source tree or verified shared package with custom Engine font/image services and the private decoder ABI. | -| `CHERYL_RMLUI_SOURCE`, `RmlUi_DIR` | Selects a RmlUi 6.3 source tree or verified shared package using Engine font services, the private rasterizer ABI and the required [placeholder correction](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract). | +| `CHERYL_RMLUI_SOURCE`, `RmlUi_DIR` | Selects a RmlUi 6.3 source tree or verified shared package using Engine font services, the private rasterizer ABI and the required [placeholder correction](../modules/rmlui.md#placeholder-dependency-contract). | | `CHERYL_MINIAUDIO_SOURCE` | Overrides pinned miniaudio source; a supplied miniaudio target takes precedence. | | `CHERYL_RMLUI_TEST_FONT=/path/to/font.ttf` | Supplies a real font fixture when the selected SDK's sample font is unavailable. | | `CHERYL_HWLOC_TARGET=existing_target` | Supplies a compatible hwloc 2.5+ (2.x) target when worker discovery/binding is selected; otherwise discovery checks supported config targets and pkg-config. | Modules reuse compatible supplied dependency targets where supported. Their owner guides document selection order and feature requirements. Owned source dependencies -are pinned in [.gitmodules](../../.gitmodules); initialize the recorded revisions. +are pinned in [.gitmodules](../../../.gitmodules); initialize the recorded revisions. HID-enabled selection first reuses a target or installed package; Linux also checks pkg-config, preferring hidraw unless configured otherwise. If none is available, configuration fetches HIDAPI 0.15.0 and needs network access or a @@ -281,7 +267,7 @@ Included Debug builds display automatically; other configurations require `--debug-terminal`. `--no-debug-terminal` disables display. Display requires a desktop and supported emulator; missing prerequisites fall back to inherited output. Headless Debug and test discovery create no viewer. See the -[Linux terminal guide](../../projects/modules/platform/debug-terminal-linux/README.md) +[Linux terminal guide](../modules/debug-terminal-linux.md) for output/report separation and lifetime policy. `ON` is a build inclusion option, not a request to display a Release window automatically. @@ -302,14 +288,15 @@ Package names vary by operating system. | OpenGL | OpenGL module | Headers/link libraries; native execution also needs a usable driver/display. | | Python and Jinja2 | GLAD generation | Jinja2 must be available in CMake's selected interpreter. | | Engine font/image services | TGUI / RmlUi | Toolkit adapters share Engine implementation instead of embedding another font/decoder copy. | -| libudev / libusb / Iconv | Linux hidapi | Requirements depend on selected HID backends; SDK consumers need the selected backend's native libraries too. | +| libudev / libusb / Iconv | Linux hidapi | Source requirements depend on the selected HID backend. SDKs package selected non-system runtimes; libudev remains a host requirement. | | Konsole or xterm | Linux output terminal at runtime | A desktop is needed for display; neither is an Engine build dependency. | -| libdw, libbfd, or libdwarf/libelf | Backward, optional | Improve trace source/symbol resolution. | +| libdw, libbfd, or libdwarf/libelf | Backward, optional | Improve trace source/symbol resolution. Selected non-system diagnostic libraries and their runtime closure are packaged in SDKs. | +| `patchelf` | Linux SDK installation/packaging | Gives each packaged native shared library an origin-relative runtime path; an ordinary source-library build does not require it. | Engine source dependencies include GLM, CTTI, spdlog, Backward, STB and JSON. Integration SDKs belong to their modules; GoogleTest is test-only. See [consumer usage requirements](consuming-engine.md#dependencies-and-headers) and the -[module index](../../projects/modules/README.md) for dependency ownership. +[module index](../../../projects/modules/README.md) for dependency ownership. ## Troubleshooting @@ -321,17 +308,33 @@ Integration SDKs belong to their modules; GoogleTest is test-only. See | Python cannot import `jinja2` | Install it in the selected interpreter or set `Python_EXECUTABLE`. | | GLFW cannot find Wayland tools/libraries | Install the requirements or set `GLFW_BUILD_WAYLAND=OFF`. | | CMake 4 rejects an old dependency policy | Set `CMAKE_POLICY_VERSION_MINIMUM=3.5`. | -| Supplied RmlUi Core lacks the placeholder-correction declaration | Use Cheryl-owned source or declare the correction on the verified dependency's Core target according to the [dependency contract](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract). | -| Standard C++ headers cannot find `math.h` or `stdlib.h` | Inspect stale compiler discovery before editing includes; see [recovery](consuming-engine.md#standard-headers-in-an-existing-build). | +| Supplied RmlUi Core lacks the placeholder-correction declaration | Use Cheryl-owned source or declare the correction on the verified dependency's Core target according to the [dependency contract](../modules/rmlui.md#placeholder-dependency-contract). | +| Standard C++ headers cannot find `math.h` or `stdlib.h` | Inspect stale compiler discovery before editing includes; see [recovery](#standard-headers-in-an-existing-build). | Successful configuration/build does not establish native platform coverage. The -[test guide](testing.md) and [architecture validation](architecture-validation.md) +[Testing and validation](testing.md) and [Component and SDK checks](architecture-validation.md) describe the relevant checks and prerequisites. -Documentation has a separate [generation entry point](documentation.md) that +Documentation has separate [tooling](documentation.md) that does not configure or compile the engine. The docs-only CMake project exposes the same targets without graphics or native development dependencies. +### 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 `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](https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_IMPLICIT_INCLUDE_DIRECTORIES.html) +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. + ## Optional worker support `CHERYL_BUILD_WORKERS=ON` with a selected feature adds `module_workers` @@ -358,13 +361,20 @@ SDK export is enabled by default and packages the same libraries used in ordinar development. Select the modules and optional features to distribute with the normal build options; omitting hwloc, for example, needs no separate SDK mode. Embedded dependencies remain inside shared Cheryl owners; static Cheryl SDKs -retain their embedded archive closure for final linking. Owned shared runtimes -such as GLFW and the reviewed UI toolkits are installed with either SDK linkage. -Windows also packages selected external DLLs/import libraries. Linux distribution -libraries remain external requirements. `CHERYL_SDK_NATIVE_PREFIX` can supply -dependency license notices; the vcpkg toolchain derives it automatically. Use a clean checkout -with submodules at their recorded revisions for reproducible archives. The packager -records the source commit and verifies recursive submodule HEADs against its gitlinks. +retain their embedded archive closure for final linking. Both linkages package +selected non-system shared runtimes and their transitive runtime dependencies. +These include FreeType, HarfBuzz, Linux ICU, selected hwloc/HIDAPI, GLFW and the +reviewed UI toolkits. Windows also supplies the corresponding native import +libraries; Windows ICU remains an operating-system dependency. + +Linux installation requires `patchelf`: copied native libraries use `$ORIGIN` to +find their packaged neighbors, and SONAME aliases stay within the SDK. Platform +and compiler runtimes remain external; see the +[consumer deployment requirements](consuming-engine.md#prebuilt-development-sdks). +`CHERYL_SDK_NATIVE_PREFIX` can supply dependency license notices; the vcpkg +toolchain derives it automatically. Use a clean checkout with submodules at their +recorded revisions for reproducible archives. The packager records the source +commit and verifies recursive submodule HEADs against its gitlinks. Use a single-configuration Ninja build and select the modules/helpers to distribute. With [shared development packages](#dependency-and-toolchain-settings), this Linux @@ -387,10 +397,11 @@ without building the demo or tests: `cheryl-sdk` builds the selected libraries and their dependencies. It does not copy headers, install a directory or create an archive. HID-enabled native input -embeds Gainput and links shared HIDAPI. Selected platform system libraries, -including Linux libudev/libusb when required by that backend, remain external. -Static Cheryl SDKs carry the corresponding final-link requirements. This packaging support does -not change the [Linux controller limitations](../../projects/modules/platform/native-glfw/README.md#controller-backend-limits). +embeds Gainput and links shared HIDAPI. SDK installation packages selected +non-system HID dependencies, including libusb when required; libudev remains a +host requirement. Static Cheryl SDKs carry the same native imports plus their +embedded archive closure. This packaging support does not change the +[Linux controller limitations](../modules/native-glfw.md#controller-backend-limits). To collect the libraries and redistributable headers into a local SDK prefix after that build: @@ -425,7 +436,7 @@ when you only need an archive. SDK installation records each shared runtime and whether the SDK supplies it or the platform must supply it. A static Cheryl SDK still needs the shared native libraries selected by the same per-library policy. Archive names include the Cheryl version from -[CherylVersions.cmake](../../cmake/CherylVersions.cmake), platform/distribution, +[CherylVersions.cmake](../../../cmake/CherylVersions.cmake), platform/distribution, architecture, compiler/version, configuration and commit ID. They also include `static` or `shared`; `SDK-MANIFEST.json` records that linkage, enabled component versions and the exact Engine artifact paths. Linux archives @@ -442,7 +453,7 @@ for ABI, logging and runtime-library requirements. ## GitHub CI and SDK artifacts -[CI and SDK artifacts](ci.md) describes the hosted build matrix, installed-consumer +[CI and SDK collection](ci.md) describes the hosted build matrix, installed-consumer checks, artifact collection, cache maintenance and Windows coverage limits. Use this build guide for local configuration and SDK production; use the CI guide to operate or investigate those workflows. diff --git a/docs/development/ci.md b/docs/site/development/ci.md similarity index 58% rename from docs/development/ci.md rename to docs/site/development/ci.md index 93aa90f3..254b6ebf 100644 --- a/docs/development/ci.md +++ b/docs/site/development/ci.md @@ -1,12 +1,13 @@ -# CI and SDK artifacts +(ci-and-sdk-artifacts)= +# CI and SDK collection -Use the hosted workflows to validate selected compositions, collect prebuilt SDKs -and maintain build caches. [Building Cheryl](building.md) owns local configuration -and SDK production; [test selection](testing.md) owns runner and fixture choices. +Use the hosted workflows to validate selected compositions, collect prebuilt SDKs, +publish eligible releases and maintain build caches. [Build engine and SDKs](building.md) owns local configuration +and SDK production; [Testing and validation](testing.md) owns runner and fixture choices. ## GitHub CI and SDK artifacts -[ci.yml](../../.github/workflows/ci.yml) validates pushes to `develop`/`main`, pull +[ci.yml](../../../.github/workflows/ci.yml) validates pushes to `develop`/`main`, pull requests targeting either branch and manual dispatches. Each job checks out recorded recursive submodules. Actions use commit pins; Windows uses the manifest's pinned vcpkg baseline. Builds use Ninja and available CPU parallelism. @@ -22,8 +23,14 @@ vcpkg baseline. Builds use Ninja and available CPU parallelism. | `linux-modules-shared` | Ubuntu 24.04 x64, GCC 14 Release | The modules/SDK composition with shared Cheryl libraries and owned dependencies. | | `windows-modules-shared` | Windows 2025 x64, MSVC RelWithDebInfo | The shared modules/SDK composition, including DLL runtime discovery. | -[run.py](../../.github/ci/run.py) configures explicit owners/options/build targets, -runs each focused owner prefix once and executes independent consumers. Modules +The `windows-dependencies` job prepares one pinned vcpkg installation for the +Windows jobs and passes it through a one-day workflow artifact. The downstream +headless, static-module and shared-module jobs consume that same preparation. + +[run.py](../../../.github/ci/run.py) configures explicit owners/options/build targets, +runs each focused owner prefix once and executes independent consumers. CI runs +its build and test phases separately so compilation and execution have distinct +results. Modules jobs compile the demo for composition coverage but never launch or package it. Linux jobs provide TrueType/CFF allocation fixtures. Missing fixtures and platform-specific skips are reported as unaccepted coverage. Native OpenGL, native @@ -31,23 +38,57 @@ hwloc binding acceptance, actual audio devices, desktop/controller QA and the Debug terminal remain separate from this focused CI selection. Modules jobs package and verify an extracted SDK on every validation run. -[verify_sdk.py](../../.github/ci/verify_sdk.py) checks the checksum and absence of +[verify_sdk.py](../../../.github/ci/verify_sdk.py) checks the checksum and absence of producer paths, then builds independent consumers/header probes using only installed Cheryl libraries. Test fixture sources/fonts come from the checkout; Cheryl and module implementation sources are not rebuilt. Use a fresh consumer build/extraction directory for each verification. See [repeating SDK checks](architecture-validation.md#installed-sdk-consumers). -Successful main-push modules jobs retain SDK candidates for three days. After all -headless and modules jobs succeed, the collection job verifies checksums and uploads -`cheryl-development-sdks-` for 90 days. Test/configuration reports are retained -for 14 days. These are workflow artifacts; the workflow creates no GitHub release, -uploads no release assets and publishes no demo package. Release publication is a -separate future decision. +Successful main-push modules jobs retain four static/shared Linux/Windows SDK +candidates for three days. After the headless, Linux-module and Windows jobs +succeed, the `sdk` job verifies those candidates and assembles this set: -The first main-push artifact collection remains -[TR20](../testing-requests.md#tr20-automated-main-push-sdk-collection); collection is -intentionally skipped on PR, `develop` and manual runs. +| Output | Contents | +| --- | --- | +| `cheryl-SDK-v-linux.tar.gz` | Shared Linux SDK. | +| `cheryl-SDK-v-windows.zip` | Shared Windows SDK. | +| `cheryl-SDK-v-static.tar.gz` | Static Linux and Windows SDKs under `linux/` and `windows/`, with a platform-selecting CMake entry point. | +| `cheryl-src-v.tar.gz` | Source and recorded recursive submodules, with `SOURCE-MANIFEST.json`. | +| `SDK-MANIFESTS.json` | Maps the four SDK candidates to their archive, prefix and original manifest, and records source identity. | +| `SHA256SUMS` | Checksums for the four archives and aggregate manifest. | + +The six files are retained as `cheryl-development-sdks-` for 90 days. +Test/configuration reports are retained for 14 days. There is no demo package. +SDK collection is skipped on PR, `develop` and manual runs. Its first hosted +main-push acceptance remains tracked by +[TR20](../../testing-requests.md#tr20-automated-main-push-sdk-collection); the +implemented workflow alone does not establish that hosted acceptance. + +### Release publication + +The separate `release` job consumes the checked six-file artifact. Publication is +enabled only for a `main` push in `cppcooper/cheryl-engine`, after SDK assembly. +[release.py](../../../.github/ci/release.py) requires all of these gates: + +1. The project version is explicitly `x.y.0` in CMake. Exactly one merged PR from + this repository's `develop` to `main` has title `Release x.y.0` matching it. +2. The checked revision is that PR's ordinary two-parent merge commit; its second + parent is the PR's develop head. Squash/rebase merges do not qualify. +3. The revision remains current `main`, and the latest documentation workflow + attempt for the same revision completes **Documentation checks** successfully. + The publisher waits up to ten minutes; Read the Docs publication is a separate + workflow and is not this gate. +4. All archives, source/submodule identities, manifests and checksums validate. + Locally marked validation packages are not publishable release inputs. + +The publisher creates `v` only when absent or already pointing to the +same revision. It creates a draft stable release with generated notes, uploads +and verifies the complete asset set, rechecks `main`, then publishes. An already +published exact match is unchanged. Conflicting tags or assets fail rather than +being moved or overwritten. Workflow concurrency serializes release publication. +These are current automation rules; a successful local assembly does not prove +GitHub permissions, artifact collection or hosted publication. ### Actions cache retention @@ -56,30 +97,40 @@ from the SDK and diagnostic artifacts above, and from each other: | Cache | Key family | Contents and ownership | | --- | --- | --- | -| Compiler results | `ccache-v2` | Main, develop and PR layers, described below. | +| Compiler results | `ccache-v3` | Compiler-family main, develop and PR layers, described below. | | X: build dependencies | `build-deps-v2` | Separate main/develop submodule Git data and Windows vcpkg package archives/downloads. PRs read their base branch's X. | | Y: Actions environment | `actions-env-cache-v2` | Shared tools, Linux package downloads, Python wheels/downloads and prepared documentation tools, published by main. | -Each compiler matrix configuration has separate ccache layers: +Compiler caches are pooled by compatible compiler family: `linux-gcc14` shares +headless/module/static/shared results, `linux-clang18` owns the Clang configuration, +and `windows-msvc` shares the Windows profiles. OS and architecture also identify +the family. Each family has these logical layers: -| Build | Readable layers | Writable layer saved after building | +| Build | Readable layers | Published destination | | --- | --- | --- | | `main` | Main baseline A | A | | `develop` | A + develop additions B | B | | PR targeting `develop` | A + B + PR additions C | C | | PR targeting `main` | A + C | C | -[restore-compiler-cache](../../.github/actions/restore-compiler-cache/action.yml) -restores the available layers into separate directories. -[compiler_cache.py](../../.github/ci/compiler_cache.py) configures ccache to check -the current layer first, followed by its parents, and write compilation results -only into the current layer. Parent hits stay in the parent layers. Statistics -live outside the archived data. Before saving, the helper removes compiled -results already available under the same cache key in a parent. It preserves -distinct lookup metadata, then trims the writable layer to 1 GiB. -An unchanged layer is not uploaded again. A layer that becomes empty replaces its -previous snapshot with a small empty-layer marker, allowing cleanup to remove -the older duplicated data. +[restore-compiler-cache](../../../.github/actions/restore-compiler-cache/action.yml) +restores available layers as separate read-only ccache storage. +[compiler_cache.py](../../../.github/ci/compiler_cache.py) gives each build job a +fresh writable delta, checks its restored current layer and then its parents, +and keeps statistics outside archived data. Parent hits stay in their original +layers. Each non-cancelled initialized worker can retain a validated, deduplicated +delta as a one-day artifact even if a later build or test step fails; retaining +cache results does not accept that failed job. + +The separate `compiler-cache` job restores the latest family layers and merges +all available worker deltas into the current layer. It validates incoming +archives before merging, preserves distinct lookup metadata, removes compiled +results already present in parents and trims the current layer to 1 GiB. Only a +changed layer is saved, using a new run-specific key because Actions caches are +immutable. An empty-layer marker records an initialized empty result. This +single publisher avoids competing matrix jobs replacing one another's additions. +When a family has no current v3 snapshot, it can import the earlier per-profile +v2 layers once; the initialized v3 snapshot supersedes that migration path. This hierarchy requires `main` to be GitHub's default branch. A PR can read its own cache, its base branch's cache and the default branch's cache; a PR targeting @@ -87,10 +138,6 @@ own cache, its base branch's cache and the default branch's cache; a PR targetin writable layer and the main baseline. These are [GitHub cache scope restrictions](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache). Missing parents cause normal cache misses until their builds populate them. -The `ccache-v2` keys distinguish this file-backend layout from the previous -ordinary local-cache archives; the first build of each configuration seeds the -new format. Compiler layers and Windows binary dependency snapshots use new -run-specific keys because saved GitHub caches are immutable. Cache X's source key hashes `.gitmodules` and the recorded submodule commits. Updating a submodule selects a new source snapshot automatically; an older X can @@ -102,8 +149,8 @@ Only main/develop push and manual runs publish X; PRs fetch or build missing dependencies locally. To update Y, change package/tool selections in -[environment.json](../../.github/ci/environment.json), generator packages in -[requirements-modules.txt](../../.github/ci/requirements-modules.txt), or the +[environment.json](../../../.github/ci/environment.json), generator packages in +[requirements-modules.txt](../../../.github/ci/requirements-modules.txt), or the documentation tool manifests. Bump `cache_version` from `2` to `3` when revising the Actions environment or forcing a refresh; this changes the family to `actions-env-cache-v3`. Environment inputs are also hashed so package changes @@ -119,7 +166,7 @@ still run on each fresh runner; cached downloads avoid fetching the same package again. Documentation tooling is prepared and cached before document generation, so a later documentation failure does not discard the prepared environment. -[cache-maintenance.yml](../../.github/workflows/cache-maintenance.yml) runs after +[cache-maintenance.yml](../../../.github/workflows/cache-maintenance.yml) runs after the build or documentation workflow finishes and daily at 04:23 UTC. It removes caches belonging to closed pull requests, keeps the newest mutable compiler and dependency snapshot per branch, configuration and cache version, and trims older caches @@ -131,7 +178,7 @@ snapshots. Known default-branch tooling caches have the same priority as develop layers. Each tier uses oldest access first. The newest X source snapshot per branch and runner receives that branch's priority; older submodule pins remain ordinary eviction candidates. Old-format compiler snapshots lose their branch -priority once the same branch and configuration has a new-format replacement. +priority once the same branch and compiler family has a v3 replacement. These priorities preserve useful branch caches where space allows; the budget can still require evicting a current layer. The budget is a target: concurrent uploads and protected caches can temporarily keep usage above it. @@ -154,7 +201,7 @@ frequent replacement of useful caches. ### Hosted Windows coverage limits -Both Windows profiles leave these focused cases unaccepted: +The Windows profiles leave these focused cases unaccepted: | Cases | Missing prerequisite and next action | | --- | --- | diff --git a/docs/development/code-style.md b/docs/site/development/code-style.md similarity index 100% rename from docs/development/code-style.md rename to docs/site/development/code-style.md diff --git a/docs/development/consuming-engine.md b/docs/site/development/consuming-engine.md similarity index 76% rename from docs/development/consuming-engine.md rename to docs/site/development/consuming-engine.md index 03ed5d4b..cbd64a7d 100644 --- a/docs/development/consuming-engine.md +++ b/docs/site/development/consuming-engine.md @@ -1,9 +1,12 @@ -# Consuming the engine +(consuming-the-engine)= +# Source and SDK integration Start with [project setup](../getting-started/project-setup.md) to choose source -or SDK consumption, then follow the [first-game walkthrough](../getting-started/first-game.md) -from startup and resource ownership through sprite/text submission. This page -provides the detailed source and SDK consumption contracts. +or SDK integration, then follow the [first-game walkthrough](../getting-started/first-game.md). +Use this guide for public target/header selection, context composition and the +complete source/SDK deployment requirements. The [module catalogue](../modules/index.md) +helps choose implementations; [Integration contracts](../architecture/interfaces-adapters.md) +defines the obligations when implementing a replacement. Applications can compose source targets from a checkout or link a prebuilt [development SDK](#prebuilt-development-sdks). Link `Cheryl::Cheryl` to use the @@ -13,7 +16,8 @@ complete configured assembly through one public include: #include ``` -That target combines Engine, configured modules and available support libraries. +That interface target forwards the usage requirements of Engine, configured modules +and available support libraries; it creates no combined binary. Its include directories and feature definitions make the matching headers visible to the compiler and the IDE. The same include works with source and SDK targets. Individual targets remain available when the application needs a smaller dependency @@ -108,7 +112,8 @@ The compatibility `Cheryl::OpenGL::Startup` target also supplies the native/grap public usage requirements. Consumers obtain C++23, includes, logging policy and SDK requirements through targets rather than compiling Engine sources or repeating dependency lists. -The [module guide](modules.md) describes ownership, standalone composition and tests. +The [module catalogue](../modules/index.md) describes available implementations. +[Module authoring](modules.md) covers standalone owners and extension rules. ## Prebuilt development SDKs @@ -181,17 +186,18 @@ mixing are outside this SDK contract. | Native dependencies | Consumer requirements | | --- | --- | -| Linux | Install the matching shared FreeType, HarfBuzz and ICU runtimes, plus hwloc/HIDAPI when selected and a compatible graphics/display stack. Owned GLFW/UI toolkit shared libraries ship with the SDK. Static Cheryl consumers additionally need the corresponding development packages and packaged embedded archives for final linking. | +| Linux | Deploy the packaged Cheryl and selected non-system native libraries together. The SDK includes FreeType, HarfBuzz, ICU, selected hwloc/HIDAPI, GLFW/UI libraries and their non-system runtime closure. The host supplies the loader/glibc, compatible C++ runtime, selected graphics/display libraries, audio service clients and libudev when selected. | | Windows | Deploy the SDK's selected Cheryl, toolkit and external native DLLs, and install the matching MSVC runtime. Native dependencies keep shared linkage with static Cheryl too. Windows 10 version 1903+ supplies ICU; no separate ICU DLL is distributed. | -Shared package configuration does not rediscover private FreeType, HarfBuzz, ICU -or hwloc development packages, but their matching runtime ABI must be available. -Static Linux packages discover those development packages for final linking and -check the producer's ICU major version. Static Windows packages resolve native -import libraries supplied with the SDK. Embedded archives and explicit system -dependencies complete static Cheryl's link closure. Native HID system/backend -requirements remain selected-platform requirements. Packaging -does not resolve the [Linux controller limitations](../../projects/modules/platform/native-glfw/README.md#controller-backend-limits). +Both static and shared SDKs import their packaged native libraries without +rediscovering private FreeType, HarfBuzz, ICU or hwloc development packages. +Both package configurations discover Threads. Static SDKs additionally supply +embedded archives for final linking and discover selected platform link +requirements: OpenGL, X11 for Native GLFW on Linux, and pkg-config/libudev when +the selected HID backend needs it. Shared SDK configurations do not perform +those additional platform package discoveries. These static final-link +development requirements differ from the packaged private dependencies. Packaging does not resolve the +[Linux controller limitations](../modules/native-glfw.md#controller-backend-limits). The package exposes `Cheryl_RUNTIME_DIRECTORIES` for its packaged shared libraries: `lib/` on Linux and `bin/` on Windows. This can be nonempty for static Cheryl SDKs. Use those paths for application runtime setup or deploy the required libraries @@ -207,18 +213,62 @@ SDK composition, including Startup and native toolkit authoring. The Desktop, GPU and physical-device coverage remain separate. The SDK does not export the Linux Debug terminal; that module retains its source-composition path. +## Context assembly + +Use `CE::Application::make_startup()` for the standard configured assembly; the +[complete application below](#minimal-native-application) shows its entry point. +For a custom assembly, supply `StartupBackend::create` to `Startup` or construct +`CE::Runtime::Context` directly. The required +`create(const CE::Runtime::ExecutionOptions&)` callback returns an owned, nonnull +context; optional `arguments` and `validate` callbacks register and check backend +options before construction. A factory returns owned components with compatible +platform and graphics domains. Header or target selection alone does not establish +that compatibility. The [Startup module](../modules/startup.md) owns its configured +backend choices, optional services and module-specific deployment requirements. + +The context owns display, presentation, renderer and resource-provider adapters, +and either owns input or explicitly borrows it. The display owns the active window; +`engine.display()`, `engine.window()`, `engine.surface()`, `engine.renderer()`, +`engine.resources()` and `engine.input()` return borrowed access without moving +work to another thread. The borrowed-input factory overload +`make_startup(input, description, options)` requires that input to survive context +cleanup/destruction and detach while its window remains live. With +`CHERYL_NATIVE_INPUT=OFF`, the standard GLFW/OpenGL composition requires +application-supplied input. Use [Integration contracts](../architecture/interfaces-adapters.md) +when supplying new implementations. + +`GameRuntime` borrows the context and game for one session. Its `run()` initializes +the adapters before calling game `init()` and closes them after game cleanup; +stopped adapters are not restarted by another session. Create graphics resources +through `engine.resources()` during platform-owned initialization or owner dispatch, +then retain their immutable generations in game state and frame packets. A retained +handle does not keep its graphics domain usable after backend closure. +[Ownership and cleanup](../architecture/ownership-cleanup.md) explains dependency +lifetimes, and [Threads and data flow](../architecture/threads-data-flow.md) explains +execution owners and result publication. + +CPU asset definitions, shader recipes, vertex data and decoded pixels are independent +of a graphics context. Construct an owned `CE::Assets::Loading::Loader(root)` for +each root; preparation needs no provider, while upload/publication uses the selected +provider on its owner. `manifests()` retains an immutable snapshot. The singleton +loader compatibility entry rejects a different root after its first initialization. +See [asset loading](../assets/asset-loading.md) for preparation/publication and +[rendering/materials](../rendering/pipelines-and-materials.md) for packet construction, +optional provider capabilities and native retirement. GUI sessions and +[audio](../runtime/audio.md) have separate application-owned lifetimes alongside +the runtime adapters. + ## Command-line startup -[`CE::Application::Startup`](../../projects/modules/runtime/startup/include/cheryl/application/startup.h) +[`CE::Application::Startup`](../../../projects/modules/runtime/startup/include/cheryl/application/startup.h) owns CLI11 parsing and validates execution, input polling and simulation timing before invoking a selected backend factory. Link `Cheryl::Startup` and include `` for the standard -`CE::Application::make_startup(description, ApplicationOptions)` path. Startup -privately owns its configured GLFW/OpenGL integration. `Cheryl::OpenGL::Startup` -remains a compatibility interface without another runtime binary. -Startup exposes CLI11 in its public option frontend; linking Engine or OpenGL -alone retains their direct context/runtime construction APIs without that public -dependency. +`CE::Application::make_startup(description, ApplicationOptions)` path. The +[Startup module](../modules/startup.md) owns backend selection, supplied services, +compatibility entry points and its deployment requirements. Startup exposes CLI11 +in its public option frontend; linking Engine or OpenGL alone retains direct +context/runtime construction 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 @@ -232,8 +282,11 @@ 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. +`make_runtime` requires an lvalue result. Keep Startup alive through the context +and game, or retain an outer `CE::Logging::ProcessLifetime`, as required by its +[logging lifetime](../modules/startup.md#lifecycle-and-execution). Backend construction +failures propagate, and `CE::Runtime::GameRuntime::run()` retains the runtime's +initialization/cleanup contract. `CE::Application::ApplicationOptions` supplies initial window/presentation, `execution` and `runtime` defaults for the generic factory. The lower-level @@ -263,15 +316,11 @@ selects finite polling; reversing them selects unlimited polling. Without overri 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. Standard selection uses -`ApplicationOptions::backend` (`Default` or `GLFWOpenGL`) and the configured -OpenGL integration accepts `--backend default|opengl`. A Startup binary built -with that integration requires the module binaries even if a run never creates -a window; omit them by building Startup without that integration. Runtime-loaded -discovery/hot unloading is outside this contract. -[The demo option helper](../../projects/apps/demo/src/demo-options.cpp) +The [context assembly path](#context-assembly) supports application-supplied +factories. Standard startup selects its compiled integration rather than discovering +runtime plugins; [the Startup module](../modules/startup.md#selection-and-dependency) +defines available selections and their linked runtime requirements. +[The demo option helper](../../../projects/apps/demo/src/demo-options.cpp) shows application-specific registration without duplicating the engine parser. ## Game hooks and ownership @@ -347,7 +396,9 @@ 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::Id`s. -Never change bindings or query a live window from concurrent `update()`. +Edit direct bindings only on the input polling owner and query live windows on +the platform owner. For runtime join/leave from concurrent `update()`, use the +queued [action-context requests](../runtime/input-state-model.md#action-contexts). To add presentation, follow [asset loading](../assets/asset-loading.md) and [pipeline/material construction](../rendering/pipelines-and-materials.md#native-bootstrap-and-binding) @@ -355,7 +406,7 @@ in `init()`, then use CPU submission helpers in frame preparation. For later replacements, prepare owned data on [workers](../runtime/worker-execution.md), [submit upload](../runtime/thread-dispatch.md) to platform and adopt only a ready complete result during update. Keep the prior generation while work is pending. -The [demo implementation](../../projects/apps/demo/src/main.cpp) shows that flow. +The [demo implementation](../../../projects/apps/demo/src/main.cpp) shows that flow. Use [input actions and focus](../runtime/input-state-model.md) for controls, [simulation timing](../runtime/simulation-timing.md) for fixed/variable policies, @@ -371,6 +422,8 @@ All public Cheryl includes share the `cheryl/` prefix in both source and SDK consumption. The files remain with their owning projects in the checkout; target usage requirements present those public roots together to the compiler and IDE. SDK installation collects the roots into one physical `include/cheryl/` tree. +CMake targets propagate the complete selected link requirements. Applications +linking raw archives directly must supply that link closure themselves. | Include | Public scope | | --- | --- | @@ -381,10 +434,11 @@ SDK installation collects the roots into one physical `include/cheryl/` tree. | `` and other `cheryl/modules/` umbrellas | One module's public headers. | | `cheryl/support/` umbrellas | One support library's public headers. | -The umbrellas list only public Cheryl headers. Some public headers transitively -use `internals/` helpers, so the SDK retains those files as implementation support; -applications do not include them directly. Including an umbrella does not enable -or link a module: CMake target selection supplies its +The umbrellas list public Cheryl headers. The SDK also retains `internals/` +support files used transitively by those declarations. The explicitly documented +`internals/exceptions.h` supplies public failure types at its historical path; +other internal support headers are not independent application interfaces. +Including an umbrella does not enable or link a module: CMake target selection supplies its dependencies and the feature definitions used by the group umbrellas. Link `Cheryl::Cheryl` for all configured components, or link granular public targets and use the same umbrellas for that narrower selection. Additional includes such @@ -443,42 +497,9 @@ the bootstrap object. The definition does not change installation scope. See the ## Independent consumers and header probes -[Engine consumer](../../projects/engine/tests/consumer/CMakeLists.txt) 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](../../projects/modules/platform/native-glfw/tests/consumer/CMakeLists.txt) -and [OpenGL consumer](../../projects/modules/graphics/opengl/tests/consumer/CMakeLists.txt) -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: - -```sh -( - 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 locate their enclosing checkout automatically; -`CHERYL_REPOSITORY_ROOT=/path/to/cheryl-engine` explicitly selects another checkout -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](architecture-validation.md) describes -independent graph, consumer and runner checks and their coverage limits. +The maintainer procedure is in +[Independent consumers and header probes](architecture-validation.md#independent-consumers-and-header-probes), +including standalone Engine/native/OpenGL consumers and their coverage limits. ## Optional output terminal @@ -488,7 +509,7 @@ The source `Cheryl::Cheryl` facade also includes it when configured; SDK facades omit this source-only module and its Debug terminal support. 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](../../projects/modules/platform/debug-terminal-linux/README.md) +See the [module guide](../modules/debug-terminal-linux.md) for argument consumption, fallback and independent viewer lifetime. The small `Cheryl::DebugTerminal::Startup` support target accepts an implementation @@ -497,16 +518,5 @@ 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 `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](https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_IMPLICIT_INCLUDE_DIRECTORIES.html) -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. +Compiler-discovery recovery is documented in +[Standard headers in an existing build](building.md#standard-headers-in-an-existing-build). diff --git a/docs/development/contributing.md b/docs/site/development/contributing.md similarity index 55% rename from docs/development/contributing.md rename to docs/site/development/contributing.md index 188c656d..65809b2b 100644 --- a/docs/development/contributing.md +++ b/docs/site/development/contributing.md @@ -1,30 +1,49 @@ # Contributing -Start with the [architecture overview](../architecture/assembling-engine.md) and the -guide for the subsystem you are changing. Choose the owning library before adding -code: neutral contracts belong in Engine, runtime integration SDKs in their -modules, optional Engine helpers in support targets, and game mechanics in the -application. The [module guide](modules.md) explains -when a new owner is useful and how to compose it. +Start with the guide for the subsystem you are changing. Choose the owning library +before adding code: neutral contracts belong in Engine, runtime integration SDKs +in their modules, optional Engine helpers in support targets, and game mechanics +in the application. [Write a module](modules.md) explains when a new owner is +useful and how to compose it. Keep a change focused on its intended behavior. Preserve unrelated work in this shared checkout and avoid changing interfaces or formatting nearby code merely because it is convenient. Apply the [C++ style](code-style.md) to affected code. Pinned dependencies retain their own licenses and upstream conventions. +```{toctree} +:hidden: +:titlesonly: + +C++ code style +``` + +## First change + +1. Find the subsystem's [Engine Systems entry](../engine-systems/index.md) and the + implementation owner; read the relevant API/subject contract before editing. +2. State the observable behavior being changed, the callers affected and any + compatibility or ownership constraint. Use the existing owner unless a new + module meets the [boundary criteria](modules.md#module-boundary-criteria). +3. Make the focused change and its directly affected regression coverage. Use the + [check selector](testing.md#choose-checks-for-a-change) to choose the smallest + checks that establish the changed contract. +4. Reconcile affected API/manual contracts during documentation maintenance, then + hand off the behavior, actual checks and outstanding acceptance separately. + ## Validation Add behavior checks under the owner they exercise. Engine checks use controlled adapters; integration checks belong with the selected module. Compile public headers through the owner's consumer probes when a change affects consumption. -Use [test selection](testing.md) to batch targets and avoid duplicate aggregate +Use [Testing and validation](testing.md) to batch targets and avoid duplicate aggregate runs. Native pixels, sound, controller reports and platform services require their own observation procedures. When reporting a change, explain the resulting behavior, why it belongs in that owner and the checks performed. Identify unresolved failures and missing coverage with their next action. Agent execution and commit rules live in -[AGENTS.md](../../AGENTS.md). +[AGENTS.md](../../../AGENTS.md). ## Documentation and planning @@ -32,12 +51,12 @@ During an authorized documentation-maintenance phase, update the authoritative API or subject guide when contracts change, including ownership, valid threads, units, preconditions and failure behavior. Keep build instructions in the [build guide](building.md), runner selection in the -[test guide](testing.md), and integration-specific details with their module. +[testing guide](testing.md), and integration-specific details with their module. Plans describe unresolved work, prerequisites, dependency boundaries and acceptance. Keep an active task's checklist in one owning plan and link it from the roadmap. Preserve useful decisions before removing completed plans; implementation history -belongs in Git. Maintain [testing requests](../testing-requests.md) only when that +belongs in Git. Maintain [testing requests](../../testing-requests.md) only when that maintenance is authorized: carry outstanding requirements in the phase handoff until then, and remove accepted requests after reconciling revision and coverage. Documentation audit reports findings without editing; maintenance implements the diff --git a/docs/development/documentation.md b/docs/site/development/documentation.md similarity index 72% rename from docs/development/documentation.md rename to docs/site/development/documentation.md index 91599d15..61678223 100644 --- a/docs/development/documentation.md +++ b/docs/site/development/documentation.md @@ -1,20 +1,31 @@ -# Generating documentation +(generating-documentation)= +# Documentation tooling The manual and C++ reference share one generation command. Doxygen extracts the selected public headers to XML; Breathe and Exhale turn that XML into Sphinx pages, and MyST includes the Markdown guides. Documentation generation does not configure -or compile the engine. +or compile the engine. Header selection comes from the public-root inventory in +[build.py](../generation/build.py), plus explicitly listed support headers. The +public exception types in `projects/engine/include/internals/exceptions.h` are +one such explicit owner; this does not make all `internals/` declarations public. The generated artifact is an HTML site rooted at -`docs/generation/generated/html/index.html`, containing both the manual and API +`docs/site/generation/generated/html/index.html`, containing both the manual and API reference. For an explicit CMake artifact target, use [`docs` in the docs-only project](#cmake-entry-points); the repository-root -[`generate-docs.sh`](../../generate-docs.sh) script provides the direct command. +[`generate-docs.sh`](../../../generate-docs.sh) script provides the direct command. Both update documentation incrementally by default; `docs-clean` or `./generate-docs.sh --clean` requests full regeneration. Enabling `CHERYL_BUILD_DOCS` makes the same targets available in an engine build; documentation is never part of the default build. +```{toctree} +:hidden: +:titlesonly: + +Publishing documentation +``` + ## Generate locally Use a Git checkout and Python 3.11 or newer with the standard `venv` module. You @@ -33,11 +44,11 @@ directory: ) ``` -Open `docs/generation/generated/html/index.html`. The output directory also +Open `docs/site/generation/generated/html/index.html`. The output directory also contains Doxygen XML, Sphinx doctrees and `sphinx-warnings.log`. `--output /path/to/output` selects another output root outside the authored documentation, or another directory beneath -`docs/generation/generated/`; its `api/`, `pycache/`, `.api-previous/` and +`docs/site/generation/generated/`; its `api/`, `pycache/`, `.api-previous/` and `.xml-previous/` directories are reserved. The generated tree is ignored by Git. Edit the declaring headers or authored guides, then regenerate. Generation is serialized within a @@ -51,19 +62,25 @@ tool-selection options. `--extract-only` generates only Doxygen XML; `--output` and `--doxygen` select an output root and explicit Doxygen executable. The Python entry point remains supported, including on Windows without Bash: -invoke `python docs/generation/build.py` from the checkout root, or use its +invoke `python docs/site/generation/build.py` from the checkout root, or use its absolute path from another directory. It accepts the same generation flags and selects tools through bootstrap. Tool-only `status` and `setup` operations remain -available through `docs/generation/bootstrap.py`; the shell script handles +available through `docs/site/generation/bootstrap.py`; the shell script handles documentation generation. The generation scripts, Sphinx configuration, extensions, styles, templates and -dependency manifests live under `docs/generation/`. Sphinx reads the authored -pages from `docs/` using that separate configuration directory. Generated API -sources live under `docs/generation/generated/api/`; Python bytecode defaults to -`docs/generation/generated/pycache/`. The other generation files are excluded from +dependency manifests live under `docs/site/generation/`. Sphinx reads the authored +pages from `docs/site/` using that separate configuration directory. Generated API +sources live under `docs/site/generation/generated/api/`; Python bytecode defaults to +`docs/site/generation/generated/pycache/`. The other generation files are excluded from the published manual's source discovery. +All authored manual prose, including module references, lives under `docs/site/`. +The module READMEs beside the code are relative symlinks to those references; +generation reads the central regular files directly. Plans, status, pending +acceptance and agent guidance remain outside the Sphinx source root. Relative +links to those repository-only files become versioned repository links in HTML. + The generator treats Sphinx warnings and unresolved references as errors, then checks the public-header inventory, private-member exclusion, local links and fragments, case-insensitive filename collisions, and source objects at the checked @@ -83,6 +100,13 @@ Sphinx's declaration inventory, then checks inline literals without reparsing headers or XML for each reference. The build log reports its indexing and linking time for serial Sphinx builds. +Public API comments can link back to a manual contract with a checked guide marker, +for example `@see [Rendering guide](cheryl-guide:rendering/pipelines-and-materials)`. +The target is the site docname without `.md`; an optional `#fragment` must name a +real heading anchor. The generator resolves it for the generated reference and +checks the result. Keep detailed declaration contracts in the declaring header +and workflows in the manual; link between them instead of copying either. + Derived pages link to the declaring base for inherited interfaces. Public nested types have their own pages rather than repeated declarations in the outer class. The reference adds Doxygen's `final`, `[[nodiscard]]` and template constraints as @@ -96,14 +120,20 @@ renderer is selected; they are not rendered diagrams. The first generation performs the complete extraction and site build. Existing output created before incremental support also needs one complete generation to establish its cache state; the normal command handles this automatically. +When relocating the documentation source root, create fresh output and doctrees +at the new default path. Retain any older output still being viewed; reusable +tool installations remain in the shared cache. Later runs reuse completed stages: - Editing a guide reuses Doxygen XML, unchanged API pages and Sphinx doctrees. Sphinx rebuilds the affected pages. CSS/template changes rewrite HTML using the cached doctrees and update the site assets. +- Changing page titles, navigation ownership, captions or table-of-contents depth + rewrites every HTML page so cached sidebars and breadcrumbs stay consistent. + This uses Sphinx's parsed navigation and retains unchanged doctrees. - Editing public headers reruns Doxygen and rebuilds affected reference pages; a changed XML file inventory starts a fresh Sphinx build. -- Changes to generation tools, configuration or the recorded source revision can +- Changes to generation tools or configuration can invalidate reuse and require a full rebuild. Each run retains the warning, link, API-inventory and source-revision checks. @@ -120,8 +150,10 @@ parallelism without changing the generated content or validation requirements. The log explains why XML or Sphinx output needs regeneration and reports the time spent extracting, rendering and checking. `Generating fresh Sphinx output` means the saved environment is being replaced; Sphinx then reports every guide -and generated API page as added. A changed Git revision requires fresh rendering -because source links are embedded in the cached document trees. +and generated API page as added. A changed Git revision refreshes repository +links and revision footers in retained HTML; it does not by itself discard the +parsed-page cache. `Navigation changed` reports a site-wide HTML rewrite using +that cache. Authored guides and included examples use Sphinx's normal modification-time tracking. If an external tool changes their contents while preserving or @@ -208,17 +240,20 @@ It enables no compiler languages and does not discover engine dependencies: ( set -e cd "$(git rev-parse --show-toplevel)" - cmake -S docs/generation -B docs/generation/generated/cmake -G Ninja - cmake --build docs/generation/generated/cmake --target docs + cmake -S docs/site/generation -B docs/site/generation/generated/cmake -G Ninja + cmake --build docs/site/generation/generated/cmake --target docs ) ``` -This incrementally updates `docs/generation/generated/html/index.html`. Select +This incrementally updates `docs/site/generation/generated/html/index.html`. Select `docs-clean` instead of `docs` in the build command to force full regeneration. The optional `docs-tools` target prepares tools; both generation targets also select them, so running `docs-tools` first is unnecessary. None of these targets belongs to the default build. +If the docs-only source project moves, configure a fresh build directory for its +new location. An existing CMake cache records the previous source directory. + To add documentation to an existing root engine build, reconfigure that same build directory and explicitly select the target: @@ -242,14 +277,24 @@ without compiling the engine. Both CMake entry points expose ( set -e cd "$(git rev-parse --show-toplevel)" - cmake -S docs/generation -B docs/generation/generated/cmake -G Ninja \ + cmake -S docs/site/generation -B docs/site/generation/generated/cmake -G Ninja \ -DCHERYL_DOCS_PYTHON=/path/to/python \ -DCHERYL_DOCS_CACHE_DIR=/path/to/shared-cache \ -DCHERYL_DOCS_FORCE_PINNED=ON - cmake --build docs/generation/generated/cmake --target docs + cmake --build docs/site/generation/generated/cmake --target docs ) ``` +## Troubleshooting + +| Symptom | Next action | +| --- | --- | +| First setup cannot download or write its environment | Check network access and the selected shared-cache permissions. `python3 docs/site/generation/bootstrap.py status` reports readiness without installing; use `--cache-dir` for a writable location and rerun setup/generation there. | +| A Sphinx warning, missing symbol or bad fragment stops generation | Inspect the selected output root's `sphinx-warnings.log` and the declaring header/guide. Repair the target or comment; do not accept an older HTML file as the current successful result. | +| API output appears stale after changing extraction rules or tools | Use [full regeneration](#incremental-rebuilds-and-full-regeneration) after correcting the inputs. Ordinary public-header edits already invalidate extraction; changing unrelated output paths is not a substitute. | +| Generation waits while another invocation is running | All output profiles share generated API sources and a checkout lock. Let the active invocation finish, then rerun; use a separate checkout for independent concurrent generation. | +| CMake retains the old docs source directory | Configure a fresh docs-only build directory for `docs/site/generation`; preserve any active build/output instead of repurposing its cache. | + ## Refreshing the pinned tools Treat a tool upgrade as a coherent change to the fallback environment, rather than diff --git a/docs/development/logging-acceptance.md b/docs/site/development/logging-acceptance.md similarity index 80% rename from docs/development/logging-acceptance.md rename to docs/site/development/logging-acceptance.md index a6b94f79..62088231 100644 --- a/docs/development/logging-acceptance.md +++ b/docs/site/development/logging-acceptance.md @@ -1,4 +1,5 @@ -# Logging acceptance +(logging-acceptance)= +# Logging checks Logging acceptance covers owned destinations, queue loss, callback reentry, retained resources and supported teardown. @@ -7,7 +8,7 @@ Configure a separate directory for each row. Use Release as the build configuration for this matrix so profile changes remain independent of the Debug sanitizer configuration. The Engine-only selection below avoids unrelated native/display prerequisites; use the -[architecture procedures](architecture-validation.md) when module composition also changes. +[Component and SDK checks](architecture-validation.md) when module composition also changes. | Row | CHERYL_LOG_PROFILE | CHERYL_LOG_COMPILED_MASK | | --- | --- | --- | @@ -64,6 +65,15 @@ checks shared loss output, and verifies the fatal case's deliberate exit code. Timeouts and nonzero results are failures, not accepted limitations. Native GPU or real-font fixtures are not prerequisites for this logging matrix. +When the runner fails, use the scenario to choose the next investigation: + +| Result | Check next | +| --- | --- | +| A required executable is missing | Build both targets for that row; the Python driver does not configure or build. | +| Expected severity records are absent | Compare the row's compiled profile/mask, runtime gates and selected destination; compile stripping cannot be undone at runtime. | +| A child times out or returns an unexpected status | Inspect that isolated scenario's output and teardown path. The deliberate fatal-child exit is accepted only when it matches the driver's expectation. | +| A fixture or platform case skips | Preserve it as missing coverage and supply its prerequisites; a successful remaining selection does not accept the skipped case. | + For concurrency changes, run relevant cases under separate sanitizer configurations. Debug currently enables ASan/UBSan. A TSan configuration must disable that combination before enabling TSan; adding it is a separate build diff --git a/docs/site/development/modules.md b/docs/site/development/modules.md new file mode 100644 index 00000000..b1ee836e --- /dev/null +++ b/docs/site/development/modules.md @@ -0,0 +1,309 @@ +(engine-and-integration-modules)= +# Module authoring + +Cheryl uses one owning `Cheryl::Engine` library and independently selected integration +libraries. Each owner declares its sources, public includes, dependencies and tests. +The root selects the assembly. The [module index](../../../projects/modules/README.md) +maps owners to Engine contracts and links their APIs; [build options](building.md#configuration-options) +control selection. + +Engine has no reverse dependency on an integration. Logging, memory, workers, +events, assets and utilities remain within the one Engine library. Platform owners +implement display/window/input contracts, graphics owners implement rendering and +resources, UI owners consume neutral contracts, and audio owners implement playback. +The optional Linux Debug terminal owns process output/viewer behavior outside the +runtime adapter graph. Its Engine support attachment accepts a module callback; +an absent implementation leaves application startup unchanged. + +The public [Startup module](../modules/startup.md), `Cheryl::Startup`, supplies +standard application bootstrap and privately integrates configured backends. +`Cheryl::OpenGL::Startup` is a compatibility interface over Startup/OpenGL rather +than another runtime library. CLI11 remains the explicit public option frontend; +Engine and OpenGL do not depend back on Startup. + +The optional [Workers module](../modules/workers.md), `Cheryl::Workers`, groups +configuration loading and topology integration with independent JSON/hwloc +features. Core pool execution/scheduling remains in Engine. Its capability-specific +`Cheryl::WorkerTopology::Hwloc` and `Cheryl::WorkerTopology::Json` compatibility +names refer to the same selected implementation. Private support belongs inside +its owning binary; configurable build policy alone is not a public module. + +| Owner | Dependency direction | +| --- | --- | +| Application | Links Engine and its selected integrations. | +| Startup | Uses Engine and privately integrates the configured OpenGL path. | +| Native GLFW | Implements Engine platform/input contracts. | +| OpenGL | Uses Engine contracts and the selected Native GLFW integration. | +| TGUI / RmlUi | Consume Engine input/rendering/font contracts; their public APIs do not require OpenGL or Native GLFW. | +| miniaudio | Implements Engine audio contracts independently of the window/graphics path. | + +```{toctree} +:hidden: +:maxdepth: 1 + +Integration contracts <../architecture/interfaces-adapters> +UI adapter authoring +``` + +## Adapter contracts + +Use [Integration contracts](../architecture/interfaces-adapters.md) to find the +required runtime interfaces, optional capabilities, existing implementations and +replacement dependencies. [Context assembly](consuming-engine.md#context-assembly) +explains how application factories connect compatible implementations. This guide +covers creating and composing the module that owns an implementation. + +### SDL3 scaffold + +`CHERYL_BUILD_NATIVE_SDL3=ON` adds the source-only +`Cheryl::NativeSDL3::Scaffold` interface target from +[`platform/native-sdl3`](../../../projects/modules/platform/native-sdl3). +The option defaults to `OFF`. Its +[`cheryl/backends/native-sdl3.h`](../../../projects/modules/platform/native-sdl3/include/cheryl/backends/native-sdl3.h) +includes the existing Engine contracts and forward-declares +`CE::Platform::SDL3::Window`, `DisplaySystem` and `InputSystem`. + +The scaffold has no runtime implementations, SDL3 dependency discovery or startup +factory. It is not registered as an SDK component or a selectable application +backend. Native GLFW remains the implemented platform integration. SDL3 runtime +implementation, OpenGL/startup selection and GLFW deprecation policy remain +future work. + +## Module boundary criteria + +A separate module is justified when it gives an application a concrete choice or +isolation benefit: omitting a dependency, selecting an alternative implementation, +or testing an external integration independently. A directory, internal interface +or small caller count does not by itself justify a library boundary. Cheryl keeps +one `Cheryl::Engine` owner with selectable static/shared linkage. + +Keep the selected Native GLFW, whole OpenGL and independent TGUI/RmlUi owners +cohesive unless a consumer demonstrates a benefit from another boundary. Native +input depends on a live window; dividing display and input splits that coupled +lifetime. OpenGL's context interface permits another window integration within its +owner and does not itself justify context/bridge libraries. + +Before introducing another library target, record: + +1. the concrete omit/replace/test benefit; +2. the engine contract the owner implements; +3. dependencies that become optional or isolated; +4. lifetime/dependency direction and how cycles are avoided; +5. test ownership and the smallest independent consumer proving the boundary; +6. compatibility consequences for current consumers. + +If those points do not identify a real benefit, keep the code inside the existing +owner. Logging, memory, workers, events, general utilities and similar internal +facilities remain engine organization unless a consumer demonstrates otherwise. +Evaluate architectural interfaces by their contract rather than deleting them +because they have few callers. + +## Select an assembly + +Application module selection, public targets, aggregate headers and deployment are +owned by [Source and SDK integration](consuming-engine.md). Use the +[module catalogue](../modules/index.md) to choose capabilities, the +[granular composition path](consuming-engine.md#granular-composition) to select +individual targets, and [Dependencies and headers](consuming-engine.md#dependencies-and-headers) +for public/private dependency requirements. The [build guide](building.md) +owns root configuration options and dependency preparation. + +Each compiled dependency has one owning Cheryl binary: Engine owns logging, +font/text and image implementations; Native GLFW owns GLFW/Gainput/HID; OpenGL +owns GLAD; each UI module owns its toolkit implementation and exports its native +authoring API. Startup owns selected integration glue and Workers owns optional +topology/configuration adaptation. Header/template usage such as GLM, CTTI and +CLI11 remains explicitly public. Preserve those boundaries when adding an owner. + +## Standalone modules + +Standalone entry points locate the enclosing checkout's shared helpers and Engine +automatically. Set an absolute `CHERYL_REPOSITORY_ROOT` when selecting another +checkout. Each owner reuses a supplied `Cheryl::Engine` or adds Engine from that +root. OpenGL also reuses `Cheryl::NativeGLFW` or requires +`CHERYL_NATIVE_GLFW_SOURCE`. Bootstrapping suppresses other owners, demo and Engine +tests in a local variable scope, without rewriting a host's cache. + +This builds only the OpenGL owner and its dependencies: + +```sh +( + set -e + cd "$(git rev-parse --show-toplevel)" + cmake -S projects/modules/graphics/opengl -B build/opengl-module -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCHERYL_REPOSITORY_ROOT="$PWD" \ + -DCHERYL_NATIVE_GLFW_SOURCE="$PWD/projects/modules/platform/native-glfw" + cmake --build build/opengl-module --target module_opengl --parallel +) +``` + +Other module directories are standalone source entry points too. Module-local +checks use `CHERYL_BUILD_TESTS=ON` without enabling Engine's broader suites. +Hosts own their root `enable_testing()` policy. Independent consumers use the +owner's `tests/consumer/` entry point. Prebuilt selected owners are also available +through the [installed SDK](consuming-engine.md#prebuilt-development-sdks), which +uses the same public aliases and its producer's compiled configuration. + +Adding a source module beside a supplied SDK leaves the SDK's `Cheryl::Cheryl` +selection unchanged. Link that source module's target explicitly alongside the +SDK facade; its public feature definition enables its umbrella in the same include. + +## Add a module + +Resolve the boundary criteria above before adding a target. Then: + +1. Create an owner under `projects/modules///` with `include/`, `src/` + and `tests/`. Keep public contracts under `include/cheryl/`; implementation + helpers and SDK details stay private where the API permits it. Add one + owner-local `include/cheryl/modules/.h` umbrella that includes its public + contracts and excludes `internals/`. Support libraries use the corresponding + `include/cheryl/support/` location. Include the owner umbrella from its selected + group in `cheryl/modules.h` or `cheryl/support.h`. +2. Declare the owner using the [linkage-aware scaffold below](#library-scaffold). + [Module.cmake.in](../../../cmake/templates/Module.cmake.in) supplies the basic + source/test structure, but currently fixes `STATIC` and omits the library + configuration helper. Apply the linkage/export steps below rather than copying + it unchanged. `cheryl_prepare_module()` reuses or bootstraps Engine; pass + `REQUIRES_NATIVE` only when Native GLFW is a real prerequisite. +3. Declare shared metadata using the same ID in the files below. Add SDK discovery + inside the selected owner, reusing compatible supplied targets before owned + sources. Publish required header dependencies with `PUBLIC`; keep private + implementation dependencies `PRIVATE`. Call + `cheryl_register_public_component(target FEATURE CHERYL_HAS_)` from + [CherylPublicHeaders.cmake](../../../cmake/CherylPublicHeaders.cmake) to register + the owner's public feature definition and make the component available. + Selection through `cheryl_select_public_component(target)` then adds it to an + owned `Cheryl::Cheryl` assembly. Use the feature definition to guard its group + umbrella include. Do not rewrite supplied targets/cache. +4. Declare a root selection with `cheryl_option(name description default_value)` + in [CherylOptions.cmake](../../../cmake/CherylOptions.cmake), and conditionally add + the owner in the root [CMakeLists.txt](../../../CMakeLists.txt), including its + `cheryl_select_public_component` call. No unrelated module should select this + owner implicitly. For SDK distribution, explicitly add its target declaration, + public-header root and feature metadata to + [CherylSdk.cmake](../../../cmake/CherylSdk.cmake); component registration alone + does not install or export a new owner. Update packaging component/version + metadata and the installed consumer alongside that export. +5. Add owner-local behavior suites and an independent consumer with first-include + probes. Keep cross-owner cases in `projects/tests/`. Document the public alias, + dependency selection, lifetime/thread requirements and capability limits in the + module's page under `docs/site/modules/`, and add it to the relevant site + navigation. Make the owner's `README.md` a relative symlink to that page. + Register the public include root in the explicit inventory in + [build.py](../generation/build.py) and add the owner's visibility decorator to + `PREDEFINED` in [Doxyfile.in](../generation/Doxyfile.in). Extraction does not + follow includes into unselected roots. Check the generated API as described in + [Documentation tooling](documentation.md). + +| Metadata file | Module declaration | +| --- | --- | +| [CherylVersions.cmake](../../../cmake/CherylVersions.cmake) | `VERSION_PROJECT_MODULE_` | +| [CherylTargets.cmake](../../../cmake/CherylTargets.cmake) | `TARGET_LIB_MODULE_` and test target identities | +| [CherylOutputs.cmake](../../../cmake/CherylOutputs.cmake) | `OUTPUT_LIB_MODULE_` and runner artifact names | +| [CherylLinkage.cmake](../../../cmake/CherylLinkage.cmake) | Scoped `LINKAGE_PUBLIC_*` / `LINKAGE_PRIVATE_*` requirements | + +Declaration files reload metadata into the caller's scope; helper definitions are +guarded. Load helpers before declaring a standalone project so versions and target +names are available. The template collects only its owner's `src/*.cpp` with +`CONFIGURE_DEPENDS`, making source additions/removals trigger regeneration. Keep +conditional implementations, generated sources and tests in their own selections. + +### Library scaffold + +This example uses an illustrative `EXAMPLE` metadata ID and `Cheryl::Example` +alias. Declare its version, target, output and linkage variables in the metadata +files above first. From `projects/modules///CMakeLists.txt`: + +```cmake +cmake_minimum_required(VERSION 3.28) +include("${CMAKE_CURRENT_LIST_DIR}/../../../../cmake/CherylModules.cmake") +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(cheryl_example_module VERSION ${VERSION_PROJECT_MODULE_EXAMPLE} LANGUAGES CXX) +endif() +if(TARGET Cheryl::Example) + return() +endif() +cheryl_configure_module_options() +cheryl_prepare_module() + +file(GLOB_RECURSE example_sources CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") +add_library(${TARGET_LIB_MODULE_EXAMPLE} ${CHERYL_LIBRARY_TYPE} ${example_sources}) +add_library(Cheryl::Example ALIAS ${TARGET_LIB_MODULE_EXAMPLE}) +cheryl_register_public_component(${TARGET_LIB_MODULE_EXAMPLE} FEATURE CHERYL_HAS_EXAMPLE) +set_target_properties(${TARGET_LIB_MODULE_EXAMPLE} PROPERTIES + VERSION "${VERSION_PROJECT_MODULE_EXAMPLE}" + OUTPUT_NAME "${OUTPUT_LIB_MODULE_EXAMPLE}" + ARCHIVE_OUTPUT_DIRECTORY "${CHERYL_BUILD_ROOT}") +cheryl_configure_library(${TARGET_LIB_MODULE_EXAMPLE} EXAMPLE) +target_include_directories(${TARGET_LIB_MODULE_EXAMPLE} PUBLIC + "$") +target_link_libraries(${TARGET_LIB_MODULE_EXAMPLE} + PUBLIC ${LINKAGE_PUBLIC_LIB_MODULE_EXAMPLE} + PRIVATE ${LINKAGE_PRIVATE_LIB_MODULE_EXAMPLE}) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + cheryl_select_public_component(${TARGET_LIB_MODULE_EXAMPLE}) +endif() +if(CHERYL_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() +if(CHERYL_BUILD_CONSUMER_TESTS) + add_subdirectory(tests/consumer) +endif() +``` + +`LINKAGE_PUBLIC_LIB_MODULE_EXAMPLE` includes `Cheryl::Engine` and any dependencies +used by public headers; private implementation libraries belong in the private +list. Register external implementation dependencies through the existing +`cheryl_private_dependency` / `cheryl_shared_dependency` helpers so ownership, +static link closure and SDK runtime collection remain visible to their owners. +See the [OpenGL CMake owner](../../../projects/modules/graphics/opengl/CMakeLists.txt) +for a complete current implementation with dependency discovery and test hooks. + +`CHERYL_LIBRARY_TYPE` follows `CHERYL_BUILD_SHARED_LIBS`. +`cheryl_configure_library(target EXAMPLE)` sets the producer's +`CE_BUILDING_EXAMPLE`, propagates `CE_EXAMPLE_STATIC` for static consumers, and +configures shared visibility, output paths, versioning and runtime paths. A shared +Cheryl module requires a shared Engine; the helper rejects a static Engine below +it. Do not replace this policy with `BUILD_SHARED_LIBS` or hand-written import flags. + +### Public symbol exports + +Give compiled public methods/functions their owning component's decorator. +`CE_API` belongs to Engine; exporting a module with it would assign the symbol to +the wrong DLL. Existing owner decorators live in +[export.h](../../../projects/engine/include/cheryl/export.h). A new owner can +provide an export header using the same policy: + +```cpp +#pragma once +#include + +#if defined(CE_EXAMPLE_STATIC) +#define CE_EXAMPLE_API +#elif defined(CE_BUILDING_EXAMPLE) +#define CE_EXAMPLE_API CE_DETAIL_EXPORT +#else +#define CE_EXAMPLE_API CE_DETAIL_IMPORT +#endif +``` + +Use `CE_EXAMPLE_API` on the compiled public functions the owner implements. +Public types whose separately exported methods require cross-library RTTI use +`CE_TYPE_API`; it preserves type visibility on supported ELF compilers and does +not replace the method decorator. Header-only templates need their definitions +available to consumers. Let the CMake target supply static/import/export flags, +and check both linkages through the owner's independent consumer. + +[CherylTests.cmake](../../../cmake/CherylTests.cmake) creates an owner aggregate with +`cheryl_add_test_aggregate(... OWNER)`; set `CHERYL_TEST_AGGREGATE` before adding +focused suites through `cheryl_add_test_suite`. Suites compile cases once into +object targets reused by focused, owner and assembly runners. Keep test hooks on +case targets so applications do not inherit them. [Test selection](testing.md) +covers runner options; [architecture validation](architecture-validation.md) +covers isolated graphs, supplied-target reuse and native acceptance. + +UI-specific font, alpha, input and publication requirements are in +[Writing a UI adapter](ui-adapters.md). diff --git a/docs/development/native-desktop-checks.md b/docs/site/development/native-desktop-checks.md similarity index 86% rename from docs/development/native-desktop-checks.md rename to docs/site/development/native-desktop-checks.md index 278d101d..c39b057e 100644 --- a/docs/development/native-desktop-checks.md +++ b/docs/site/development/native-desktop-checks.md @@ -1,4 +1,19 @@ -# Desktop smoke checks +(desktop-smoke-checks)= +# Desktop and controller checks + +These are interactive observations of a current native build, separate from +controlled-adapter checks. Prepare a usable display/GPU, readable input devices +for controller checks and the configured assets/fonts. Start with the demo's +[native QA preparation](../../../projects/apps/demo/README.md#native-qa-preparation) +when no matching executable exists; its configuration and explicit `demo` build +step establish the launch prerequisite. A configured directory alone is not a +refreshed executable. + +Use the [repeatable sequence](#repeatable-sequence) for rendering/input/reload and +normal shutdown, [Linux controller automation](#linux-controller-automation) for +the synthetic backend cases, and the physical-controller procedure below for real +reports. Record the source/configuration, actions and observations; a clean exit +alone does not establish visible or physical behavior. ## Repeatable sequence @@ -17,10 +32,10 @@ In each mode, check these observations: 2. In a toolkit-free build, press F2, type ASCII text, move the caret and use Backspace/Delete. Text focus prevents those keystrokes from moving the camera; Enter/Esc releases it. With UI adapters selected, use their - [interaction sequence](../../projects/apps/demo/README.md#interaction-checks); + [interaction sequence](../../../projects/apps/demo/README.md#interaction-checks); Escape releases UI focus. The builtin HUD uses the Unicode text service. Its multilingual, bidi/fallback and wrapping scope is maintained in the - [demo procedure](../../projects/apps/demo/README.md#unicode-preview-validation); this existing input + [demo procedure](../../../projects/apps/demo/README.md#unicode-preview-validation); this existing input sequence alone does not establish them. Probe editing still uses logical scalars. 3. Test indexed shader reload using the temporary-root launch block below. Press Escape to leave text editing, then F5 with valid definitions/sources. Append @@ -35,7 +50,7 @@ In each mode, check these observations: reloads. The process exits normally without hanging or printing a cleanup error. This block uses the refreshed `build/testing-graphics/demo` from the -[demo QA preparation](../testing-requests.md#tr19-qa-indexed-demo-materials-and-recovered-graphics), +[demo QA preparation](../../testing-requests.md#tr19-qa-indexed-demo-materials-and-recovered-graphics), or a matching native demo. It copies only shader/UI/font inputs, preserving the checkout and omitting the assets repository's Git metadata and bulk collections. Optional tile/sprite samples skip in this minimal root; that is not artwork QA. @@ -75,7 +90,7 @@ aggregates. Automation does not establish HID readiness or physical reports. Reuse the matching `build/testing-native-linux` directory and refresh its demo for targeted text/artwork observations; the demo's -[QA preparation](../../projects/apps/demo/README.md#native-qa-preparation) builds +[QA preparation](../../../projects/apps/demo/README.md#native-qa-preparation) builds only that target. When controller source or configuration requires new coverage, this procedure batches the relevant targets and exports the owned Gainput manager's compiler command. With HID disabled its HID macro arguments must be absent; inspect @@ -134,7 +149,7 @@ PY Use the existing `build/testing-native-linux/demo` with developer logging, native input and GLFW's X11 backend selected, both optional UI adapters disabled, and `GAINPUT_ENABLE_HID=OFF`. This checks the -[Linux joystick path](../../projects/modules/platform/native-glfw/README.md#linux-controller-mapping); +[Linux joystick path](../modules/native-glfw.md#linux-controller-mapping); it does not establish direct HID report/output behavior or Windows notifications. The physical acceptance configuration is a Bluetooth DualSense. Other controllers and USB connections require their own observations. diff --git a/docs/development/publishing-documentation.md b/docs/site/development/publishing-documentation.md similarity index 88% rename from docs/development/publishing-documentation.md rename to docs/site/development/publishing-documentation.md index 8729a374..26304c74 100644 --- a/docs/development/publishing-documentation.md +++ b/docs/site/development/publishing-documentation.md @@ -1,16 +1,27 @@ # Publishing documentation Publish the generated manual and API as a versioned Read the Docs site. Prepare -the artifact with [documentation generation](documentation.md), then use the +the artifact with [Documentation tooling](documentation.md), then use the account, version and verification procedures below. Hosted setup requires the repository/account permissions stated in the setup procedure. +Before the first publication: + +1. Generate/check the intended revision using [Documentation tooling](documentation.md). +2. Complete the [account/site setup](#first-account-and-site-setup), recording its + actual project URL and desired active versions. +3. Confirm the [version policy](#version-policy) and separately approve stable + promotion before enabling its repository variable. +4. Apply the [publication checks](#publication-verification-and-recovery) to the hosted URL, including + exact revision, navigation/search and version selection. Local HTML success + does not establish hosted delivery. + ## Publishing contract The publishing configuration is -[.readthedocs.yaml](../../.readthedocs.yaml), -[documentation.yml](../../.github/workflows/documentation.yml) and -[sync-docs-stable.py](../../.github/scripts/sync-docs-stable.py). Hosted publication +[.readthedocs.yaml](../../../.readthedocs.yaml), +[documentation.yml](../../../.github/workflows/documentation.yml) and +[sync-docs-stable.py](../../../.github/scripts/sync-docs-stable.py). Hosted publication still requires the account and repository setup below. Both `develop` and `main` must contain the engine headers and documentation implementation before their respective sites can build. @@ -44,7 +55,7 @@ build: jobs: build: html: - - python docs/generation/bootstrap.py exec --force-pinned -- python docs/generation/build.py --clean --output "$READTHEDOCS_OUTPUT" + - python docs/site/generation/bootstrap.py exec --force-pinned -- python docs/site/generation/build.py --clean --output "$READTHEDOCS_OUTPUT" ``` Keep `sphinx`, `mkdocs` and a separate Python installation step out of this custom diff --git a/docs/development/testing.md b/docs/site/development/testing.md similarity index 70% rename from docs/development/testing.md rename to docs/site/development/testing.md index a0cd244c..e897252c 100644 --- a/docs/development/testing.md +++ b/docs/site/development/testing.md @@ -1,10 +1,37 @@ -# Building and selecting tests +(building-and-selecting-tests)= +# Testing and validation Tests belong to the owner whose behavior they exercise. Engine uses controlled contract implementations; modules test their real integrations; `projects/tests/` -owns cross-module behavior. See [architecture validation](architecture-validation.md) +owns cross-module behavior. See [Component and SDK checks](architecture-validation.md) when changing isolation, composition or lifetime boundaries. +```{toctree} +:hidden: +:titlesonly: + +Component and SDK checks +Logging checks +Desktop and controller checks +``` + +## Choose checks for a change + +| Changed contract | Start with | Add when applicable | +| --- | --- | --- | +| Engine algorithm or lifecycle behavior | The affected focused suite in `tests-engine`. | Engine acceptance for cross-thread, failure or teardown boundaries. | +| Integration implementation | Its module's focused runner. | Coexistence checks when another owner participates; native/device observations for the actual backend. | +| Public header, target or dependency boundary | The owner's consumer and first-include probes. | Standalone/supplied-target checks when composition changes. | +| SDK installation or deployment | [Installed SDK consumers](architecture-validation.md#installed-sdk-consumers). | Static/shared and platform variants affected by the packaging change. | +| Logging failure or lifetime policy | [Logging checks](logging-acceptance.md). | The affected compile-profile rows and isolated failure drivers. | +| Visible desktop/controller behavior | [Desktop and controller checks](native-desktop-checks.md). | The relevant automated adapter/native regressions; neither substitutes for the other. | + +A **first-include probe** compiles a small translation unit with one public header +included first. It catches missing direct includes and unintended dependency +leakage. Its object-only success establishes header self-containment; the linked +consumer checks symbols and usage requirements, and runtime checks establish +behavior. Choose the layers affected by the change. + ## Runner selection `CHERYL_BUILD_TESTS=ON` selects focused GoogleTest runners and makes broader runners @@ -26,7 +53,7 @@ acceptance runners. Both require tests. Standalone module tests default to `OFF` Aggregates reuse focused and GoogleTest acceptance case objects. Build targets use `all-*`, but their executables use `tests-all-*`; `all-tests` produces `tests-all`. `tests-logging` produces `tests-engine-logging`. Other focused executable names -match their build targets. The [owner guides](../../projects/modules/README.md) +match their build targets. The [owner guides](../../../projects/modules/README.md) describe their coverage. Linux's separate `tests-native-joystick` uses syscall wrapping and stays outside aggregates. @@ -58,13 +85,13 @@ targets, and keep tests that share mutable files in isolated working directories | --- | --- | --- | | `acceptance-engine` | `tests-acceptance-engine` | Runtime, concurrency, failure and resource GoogleTests. | | `acceptance-opengl` | `tests-acceptance-opengl` | Native graphics/runtime GoogleTests; requires native input selection. | -| `acceptance-logging` | `tests-acceptance-engine-logging` | Fault/timeout/static-teardown driver via [logging acceptance](logging-acceptance.md). | +| `acceptance-logging` | `tests-acceptance-engine-logging` | Fault/timeout/static-teardown driver via [Logging checks](logging-acceptance.md). | | `acceptance-signal` | `tests-acceptance-engine-signal` | POSIX crash-bootstrap driver in Debug/Release. | | `acceptance-worker-topology-hwloc` | `tests-acceptance-worker-topology-hwloc` | Host topology/masks, inherited eligibility and CPU binding without memory placement; requires selected hwloc support and native capabilities. | | `backward-cpp` | `tests-dependency-backward-cpp` | Standalone dependency trace check. | Manual logging and signal drivers stay outside GoogleTest aggregates and CTest; -[architecture validation](architecture-validation.md#manual-acceptance-drivers) +[Component and SDK checks](architecture-validation.md#manual-acceptance-drivers) owns their invocations. `CHERYL_BUILD_CONSUMER_TESTS=ON` selects consumers independently of GoogleTest. @@ -72,7 +99,7 @@ Each consumer also builds its object-only first-include header probes. The Engin targets are `consumer-cengine` and `consumer-headers-cengine`; modules list their consumer targets in their owner guides. Configure each `tests/consumer/` entry point independently to establish standalone composition; a root consumer alone -cannot prove that boundary. See [independent consumers](consuming-engine.md#independent-consumers-and-header-probes). +cannot prove that boundary. See [independent consumers](architecture-validation.md#independent-consumers-and-header-probes). Selected worker support adds `consumer-worker-topology` and its neutral support-header probes; CTest registers that consumer under its exact target name without a suite @@ -100,12 +127,12 @@ identify skipped fixtures/capabilities as unresolved coverage. The Linux module adds `tests-acceptance-engine-terminal` and `tests-acceptance-engine-terminal-reports` when its tests are selected. They remain outside CTest/GoogleTest aggregates and are driven by -[terminal.py](../../projects/modules/platform/debug-terminal-linux/tests/acceptance/terminal.py), +[terminal.py](../../../projects/modules/platform/debug-terminal-linux/tests/acceptance/terminal.py), which selects deliberate failure/termination cases. The accepted -[configuration matrix](../../projects/modules/platform/debug-terminal-linux/README.md#configuration-matrix) +[configuration matrix](../modules/debug-terminal-linux.md#configuration-matrix) is reusable from the module guide; actual desktop observations remain -[TR18](../testing-requests.md#tr18-qa-linux-debug-terminal-on-a-real-desktop). -The [module guide](../../projects/modules/platform/debug-terminal-linux/README.md#test-reporting) +[TR18](../../testing-requests.md#tr18-qa-linux-debug-terminal-on-a-real-desktop). +The [module guide](../modules/debug-terminal-linux.md#test-reporting) explains saved runner reporting, discovery/headless suppression and supplied-GoogleTest fallback. Debug consumers can disable display with `--no-debug-terminal` when attached; omitted configurations pass that option through unchanged. @@ -119,9 +146,9 @@ attached; omitted configurations pass that option through unchanged. - RmlUi checks require its SDK sample font or an explicit `CHERYL_RMLUI_TEST_FONT`. - CPU text, tile and offline audio checks do not establish appearance or device output. Physical observations have separate procedures in the - [desktop guide](native-desktop-checks.md) and [audio guide](../../projects/modules/audio/miniaudio/README.md#acceptance-boundaries). + [Desktop and controller checks](native-desktop-checks.md) and [audio guide](../modules/miniaudio.md#acceptance-boundaries). An empty selection, skipped case or missing device leaves that coverage pending. Report the revision, configuration, platform and unavailable observations. The -[testing queue](../testing-requests.md) tracks pending checks and QA; owning plans track +[testing queue](../../testing-requests.md) tracks pending checks and QA; owning plans track implementation progress. Durable limitations belong in the relevant subject guide. diff --git a/docs/development/ui-adapters.md b/docs/site/development/ui-adapters.md similarity index 80% rename from docs/development/ui-adapters.md rename to docs/site/development/ui-adapters.md index 964fd7d0..5a296411 100644 --- a/docs/development/ui-adapters.md +++ b/docs/site/development/ui-adapters.md @@ -1,10 +1,11 @@ -# Writing a UI adapter +(writing-a-ui-adapter)= +# Write a UI adapter A UI adapter is an optional module that translates a toolkit into Cheryl's existing input, render and resource contracts. The toolkit owns its widgets, documents, styling and layout. Engine does not include toolkit headers or select -an adapter. The [TGUI](../../projects/modules/ui/tgui/README.md) and -[RmlUi](../../projects/modules/ui/rmlui/README.md) modules provide independent +an adapter. The [TGUI](../modules/tgui.md) and +[RmlUi](../modules/rmlui.md) modules provide independent examples under `CE::UI::TGUI` and `CE::UI::RmlUi`. Their public classes are toolkit-specific, not requirements for other adapters. @@ -43,6 +44,7 @@ resolve the smallest demonstrated seam first. | --- | --- | --- | | Draw geometry and clipping | [Render submission](../rendering/pipelines-and-materials.md) | Record ordered, owned colored triangles and logical rectangular clips. Match the toolkit's alpha convention to the supplied material. | | Images and font atlases | [Immutable resource publication](../resources/resource-residency.md#immutable-resource-publication) | Copy CPU pixels into immutable generations; publish provider-owned resources on the platform owner. Resolve font ownership and sampling before exposing textures. | +| Font selection and loaded data | [Engine font service](../text/index.md) | Consume the configured default/catalogue and explicit retained `Font` handles; translate byte/index/style ownership into native toolkit fonts. | | Keyboard, pointer and text | [Input records and focus](../runtime/input-state-model.md) | Translate portable identities and committed text without native input APIs, invented characters or modification of another reader's records. | | Time, size and OS services | [Runtime ticks](../runtime/runtime-frame-boundary.md) and capability reporting | Use copied window/framebuffer dimensions and explicit simulation time; report unavailable clipboard, cursor and IME services. | | Queued upload and retained playback | [Thread dispatch](../runtime/thread-dispatch.md) and [native retirement](../resources/resource-residency.md#native-retirement-and-maintenance) | Send owned preparation through platform submission and retain complete published scenes through replacement and teardown. | @@ -58,9 +60,18 @@ Applications may pass compatible material handles loaded from JSON or constructe manually as explicit overrides. The adapter validates vertex layout, topology, alpha/depth/culling state, projection semantic and texture parameters. It does not create OpenGL pipelines or query a native context. Unsupported providers reject -default construction while explicit compatible overrides remain usable. Font -rasterization may belong to the toolkit; its resulting atlas still follows the -same CPU-copy and resource-publication boundary. +default construction while explicit compatible overrides remain usable. Engine +owns the loaded font bytes and face selection; native atlas/effect behavior stays +with the toolkit. An atlas follows the same CPU-copy and resource-publication boundary. + +The context-aware sessions import `Context::fonts()` once and establish native +default inheritance. Explicit `Font` handles select registered families through +session helpers without source-path extraction. Preserve actual face indices and +style metadata when registering memory with a toolkit, and retain that memory +until native shutdown. Use the service's ordered `fallbacks()` snapshot for a +toolkit with global glyph fallback; importing every catalogue family as a fallback +would make specialty fonts affect unrelated text. Reconfiguration does not rebind +existing sessions. A standalone input-only constructor remains available. Expanded triangles suffice for the current adapters. Indexing, batching and draw sorting require their own consumer need; preserve authored world/HUD/overlay order. @@ -77,7 +88,7 @@ The independent implementations differ where their toolkits differ: | --- | --- | --- | | Authoring | Native GUI/widgets; optional typed numeric placement and resizing. | Native context/documents; RML and RCSS layout, including percentage bounds and flex. | | Alpha | Straight vertex/image RGBA and `StraightAlpha` materials. | Premultiplied output and `PremultipliedAlpha` materials; file images convert once. | -| Fonts | Engine rasterization through the custom TGUI font backend; embedded default font. | Module-owned interface retaining reviewed provider/atlas/effects/fallback over Engine rasterization; explicitly registered owned font bytes. | +| Fonts | Configured Engine default/catalogue; retained face indices, real bold faces and glyph fallback through a custom backend. Native italic style uses geometry; explicit `FontStyle` selects an italic face. | Configured Engine default/catalogue; retained byte/index/style registration under native RCSS family names and the configured common glyph fallback chain. | | Sampling | Each draw retains nearest/linear smoothing independently of pixels; UI samplers clamp, with no mipmaps or anisotropy. | Existing image upload defaults; repeating UV/effect extensions reject. | | Toolkit lifetime | One guarded process-global backend. | One guarded Core initialization with contexts and interfaces. | @@ -95,7 +106,8 @@ when an implemented consumer demonstrates the requirement. 1. On platform, prepare the resource upload endpoint. Default materials are created lazily on that owner during the first nonempty upload. -2. On simulation/UI, create the toolkit session and native presentation objects. +2. Configure Engine fonts before consumers. On simulation/UI, create the toolkit + session from the Engine context, then native presentation objects. Keep all toolkit calls on that owner, including font and layout changes. 3. Each update supplies copied logical/framebuffer sizes, simulation time and the immutable input stream. Logical UI coordinates and framebuffer pixels remain @@ -161,12 +173,12 @@ record consumption or stale-lease interference, ordered retained passes, and resources surviving either toolkit's teardown. Keep shared functionality in Engine only when these independent implementations demonstrate its purpose. -The repository's [coexistence suite](../../projects/tests/ui-coexist/CMakeLists.txt) -owns that cross-project proof. The [demo](../../projects/apps/demo/README.md) +The repository's [coexistence suite](../../../projects/tests/ui-coexist/CMakeLists.txt) +owns that cross-project proof. The [demo](../../../projects/apps/demo/README.md) selects either adapter or both and supplies separate focus controls. Each module's standalone consumer and implementation checks remain independent. -The [demo interaction guide](../../projects/apps/demo/README.md#interaction-checks) +The [demo interaction guide](../../../projects/apps/demo/README.md#interaction-checks) owns native observations and selected-platform limits. A new backend, platform or supplied package needs its own applicable proof. @@ -215,22 +227,30 @@ one owner is affected. A matching compiled build can be reused without rebuildin Inspect the compile inventory when the RmlUi correction changes: Core must compile the generated `rmlui-fixes/WidgetTextInput.cpp` rather than the unguarded original. -The recorded root session/editing/lifetime/coexistence baseline is accepted, but does -not directly exercise `CE::Application::Startup`'s parser/backend factory, support headers, the new -`before_record` overload or placeholder hit-testing. These gaps remain in the -[owning checklist](../planning/develop-review-and-development-plan.md#startup-and-ui-follow-up); -font-weight fixture gaps remain in the [Unicode plan](../planning/short-term/unicode-text.md#progress). +The recorded root session/editing/lifetime/coexistence baseline is accepted. +The separate [Startup suite](../../../projects/modules/runtime/startup/tests/src/startup.cpp) +covers parser validation, factory forwarding and help/error paths before context +creation. Successful context/result/runtime handoff, the `before_record` overload +and placeholder hit-testing still need dedicated regressions in the +[owning checklist](../../planning/short-term/README.md#startup-and-ui-follow-up); +CPU and adapter fixtures cover registered weight/style selection and face indices; +automatic discovery's numeric-weight boundary cases remain in the +[font-selection plan](../../planning/short-term/README.md#font-selection-regressions). These checks do not +establish native visual correctness of distinct authored style faces. Corrected supplied RmlUi targets/packages require an independent consuming host and declaration/rejection checks under the -[module contract](../../projects/modules/ui/rmlui/README.md#placeholder-dependency-contract). +[module contract](../modules/rmlui.md#placeholder-dependency-contract). Owned-source success does not establish those supplied paths or standalone composition. Native startup, focus, placeholder hit-testing, layout and shutdown observations are accepted in the selected Linux root composition. The -[demo guide](../../projects/apps/demo/README.md#startup-and-ui-validation) retains +[demo guide](../../../projects/apps/demo/README.md#startup-and-ui-validation) retains the reusable native procedure. Builtin Unicode and sample artwork keep their own coverage limits there; dedicated deterministic cases and supplied dependencies remain outside that native baseline. -Sampling and shared indexed-material/resource changes postdate that accepted -baseline. Their targeted reruns are in the [testing queue](../testing-requests.md); -the earlier observations do not establish acceptance of the changed code. +Current sampling, retained-frame and positioned-text acceptance is recorded with +its reusable procedure in [Component and SDK checks](architecture-validation.md#sampling-and-positioned-text). +Keep that evidence separate from supplied-toolkit composition, distinct authored +font-face appearance and other desktop/material-failure observations. The +[testing queue](../../testing-requests.md) retains the outstanding observations; +an earlier native baseline does not establish a later changed path. diff --git a/docs/diagnostics/index.md b/docs/site/diagnostics/index.md similarity index 60% rename from docs/diagnostics/index.md rename to docs/site/diagnostics/index.md index 45e0f338..fe09c4ac 100644 --- a/docs/diagnostics/index.md +++ b/docs/site/diagnostics/index.md @@ -1,15 +1,22 @@ -# Diagnostics +(diagnostics)= +# Logging and diagnostics Configure useful output, inspect subsystem observations, and preserve failure context through cleanup. The [Diagnostics system](../engine-systems/diagnostics.md) -introduces the available snapshots and their observation boundaries. +introduces the available snapshots and their observation boundaries. Start with +logging for missing records or destination/filter settings, subsystem diagnostics +for a stalled or unexpected runtime/resource state, and failure reporting when an +operation throws or cleanup also fails. The output viewer displays process output; +it does not replace the log files or saved failure report. ```{toctree} :maxdepth: 1 +:titlesonly: Configure logging and investigate missing records <../runtime/logging> Investigate runtime and resource symptoms <../runtime/subsystem-diagnostics> Handle native and cleanup failures <../runtime/failure-reporting> +Linux output viewer <../modules/debug-terminal-linux> ``` A log record or counter is evidence about an operation, not a completion barrier. diff --git a/docs/engine-systems/assets.md b/docs/site/engine-systems/assets.md similarity index 85% rename from docs/engine-systems/assets.md rename to docs/site/engine-systems/assets.md index 2866072b..2a13f6d6 100644 --- a/docs/engine-systems/assets.md +++ b/docs/site/engine-systems/assets.md @@ -17,7 +17,8 @@ Types below are in `CE::Assets` unless another namespace is shown. | --- | | {cpp:struct}`Loading::Loader `, {cpp:struct}`Loading::Prepared `
Prepare owned CPU input, upload on the provider owner, or synchronously `load_assets()`. [Loading entry point](../assets/asset-loading.md#runtime-entry-point); [publication and retry](../assets/asset-loading.md#publication-and-retry). | | {cpp:class}`Loading::FileRegistry `, {cpp:struct}`Manifests::Graphics `
Discover indexed files and retain parsed manifest metadata. [Index selection](../assets/asset-manifests.md#index-selection-and-paths); [typed consumers](../assets/asset-loading.md#typed-manifest-data-and-consumers). | -| {cpp:struct}`Sprites::Manager `, {cpp:struct}`Tilesets::Manager `
Retrieve shared sprite/tile assets; validate handles before use. [Resources and caches](../assets/asset-values-and-playback.md#resources-and-caches); [sprite task](../getting-started/how-to/draw-sprite.md). | +| {cpp:struct}`Sprites::Manager `, {cpp:struct}`Tilesets::Manager `
Retrieve shared sprite/tile assets; validate handles before use. [Resources and caches](../assets/asset-values-and-playback.md#resources-and-caches); [sprite walkthrough](../getting-started/first-game.md#put-a-sprite-on-screen). | +| {cpp:struct}`Graphics::Asset `
Load a whole image or wrap an uploaded image as a retained quad with a default material. [Draw a whole image](../getting-started/how-to/draw-sprite.md#draw-a-whole-image). | | {cpp:struct}`Sprites::Definition `, {cpp:struct}`Tilesets::Definition `, {cpp:struct}`Grids::Definition `
Describe immutable atlas grids, cells, clips and tile rules. [Grid coordinates](../assets/asset-values-and-playback.md#coordinates-and-grid-geometry); [manifest schema](../assets/asset-manifests.md). | | {cpp:class}`Sprites::Animation `, {cpp:struct}`Tilesets::Animation `
Keep independent playback cursors over shared clip metadata. [Sprite playback](../assets/asset-values-and-playback.md#sprite-playback); [tile playback](../assets/asset-values-and-playback.md#tile-playback-and-rules). | @@ -50,7 +51,7 @@ its own font-selection/layout path. | Types | Systems | | --- | --- | -| [`Tutorial::Player`](../getting-started/create-a/player.md)
[`Tutorial::Enemy`](../getting-started/create-a/enemy.md)
[`FirstGame::Game`](../getting-started/first-game.md)
{cpp:struct}`CE::Rendering::DrawPacket2D` | [Rendering](rendering.md)
[Text](text.md) | +| [`Tutorial::Character`](../getting-started/create-a/character.md)
[`FirstGame::Game`](../getting-started/first-game.md)
{cpp:struct}`CE::Rendering::DrawPacket2D` | [Rendering](rendering.md)
[Text](text.md) | ## Contracts @@ -90,18 +91,20 @@ loader.load_assets(engine_.resources()); auto sprite = CE::Assets::Sprites::Manager::get().get_asset("first-game", "actor"); ``` -Check `sprite` and required materials before constructing your player. Keep its -position/animation cursor in the player, not in shared asset metadata. See -[loading and drawing a sprite](../getting-started/how-to/draw-sprite.md). +Check `sprite` and required materials before constructing your player; a lookup +can return an empty handle. The [loading failure contract](../assets/asset-loading.md#publication-and-retry) +explains which earlier entries remain published after an upload error. Keep +player position and animation state in the player, not in shared asset metadata. See +[loading and drawing a sprite](../getting-started/first-game.md#put-a-sprite-on-screen). ## Limits and further reading -There is no engine-owned map/world or built-in Player/Enemy class. Tile selection +There is no engine-owned map/world or prescribed controller/Character class. Tile selection operates on application-owned map data. Read [loading](../assets/asset-loading.md), [manifests](../assets/asset-manifests.md), [asset values and playback](../assets/asset-values-and-playback.md), [file/font discovery](../assets/file-and-font-discovery.md), [asset catalog](../assets/catalog.md) and [residency](../resources/resource-residency.md). The -[Loader header](../../projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h) +[Loader header](../../../projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h) defines preparation/publication entry points. diff --git a/docs/engine-systems/audio.md b/docs/site/engine-systems/audio.md similarity index 92% rename from docs/engine-systems/audio.md rename to docs/site/engine-systems/audio.md index 5a057e91..f4842d33 100644 --- a/docs/engine-systems/audio.md +++ b/docs/site/engine-systems/audio.md @@ -28,6 +28,12 @@ Types below are in `CE::Audio` unless another namespace is shown. | Device / offline output | Device output advances independently; offline output advances only through explicit `render()` calls. [Backend selection](../runtime/audio.md#backends-and-limits); [miniaudio preparation](../modules/miniaudio.md#preparation-and-output). | | Clip / streamed file | Retained decoded samples or file-backed playback whose source must remain available. [Sources and limits](../runtime/audio.md#clips-and-voices). | +Use a decoded clip when several effects should share retained samples without +reopening a file. Stream longer tracks when retaining the whole decoded source +is undesirable; keep that file available during playback. The +[miniaudio preparation example](../modules/miniaudio.md#preparation-and-output) +shows both entry points. + ## Usage ### Uses @@ -97,4 +103,4 @@ Offline processing is explicit; an unavailable device must not silently succeed inaudibly. Spatial/world/collision behavior belongs to game code or another selected capability. Read [audio ownership](../runtime/audio.md), [miniaudio setup and coverage](../modules/miniaudio.md) and -[the System/Voice interface](../../projects/engine/include/cheryl/audio/system.h). +[the System/Voice interface](../../../projects/engine/include/cheryl/audio/system.h). diff --git a/docs/engine-systems/diagnostics.md b/docs/site/engine-systems/diagnostics.md similarity index 95% rename from docs/engine-systems/diagnostics.md rename to docs/site/engine-systems/diagnostics.md index f259bf1a..84b6a0d8 100644 --- a/docs/engine-systems/diagnostics.md +++ b/docs/site/engine-systems/diagnostics.md @@ -91,7 +91,9 @@ const auto rendered = stats.rendered; Use these values in an application report with the run outcome. Different update/render counts are expected with independent scheduling and skipped or -superseded visual snapshots. +superseded visual snapshots. Use the +[symptom, evidence and action table](../runtime/subsystem-diagnostics.md#investigate-a-runtime-symptom) +to decide which observation to inspect before changing a scheduling or resource policy. ## Limits and further reading @@ -99,4 +101,4 @@ Counters do not measure cache speedups, native thread residency or unselected hardware behavior. Read [subsystem observation limits](../runtime/subsystem-diagnostics.md), [failure reporting](../runtime/failure-reporting.md), [architecture acceptance limits](../development/architecture-validation.md#evidence-limits) -and [diagnostic declarations](../../projects/engine/include/cheryl/core/diagnostics.h). +and [diagnostic declarations](../../../projects/engine/include/cheryl/core/diagnostics.h). diff --git a/docs/engine-systems/dispatch.md b/docs/site/engine-systems/dispatch.md similarity index 86% rename from docs/engine-systems/dispatch.md rename to docs/site/engine-systems/dispatch.md index b4bacdee..29225380 100644 --- a/docs/engine-systems/dispatch.md +++ b/docs/site/engine-systems/dispatch.md @@ -79,15 +79,22 @@ there is no total order between independent platform, simulation and worker work ## Integration -From another producer, submit a window observation to its owner: +During `init()`, obtain a copyable endpoint and retain it in the producer that +will submit requests. During the live session that producer can request a window +observation on its owner: ```cpp -auto pending = engine_.platform_dispatcher().submit([](CE::Runtime::Context& owner) { +auto platform_requests = engine_.platform_dispatcher().submission(); // Save during init(). +// Later, the producer uses its saved endpoint: +auto pending = platform_requests.submit([](CE::Runtime::Context& owner) { return owner.window().framebuffer_size(); }); ``` -Inspect future readiness from later updates; do not block simulation while it +The endpoint and `pending` are retained values, not temporary service pointers. +[The complete readiness example](../runtime/thread-dispatch.md#submit-and-adopt-a-result) +shows their member storage and adoption during update. Inspect future readiness +from later updates; do not block simulation while it depends on platform progress. Ordinary game layout should use tick dimensions directly; the example shows where an explicitly requested live observation runs. @@ -97,4 +104,4 @@ Dispatch is not a general executor, a completion barrier based on queue emptines or automatic lifetime extension for captured raw pointers. Read [thread dispatch](../runtime/thread-dispatch.md), [runtime shutdown](../runtime/runtime-frame-boundary.md#shutdown) and -[the platform interface](../../projects/engine/include/cheryl/core/engine/platform-dispatcher.h). +[the platform interface](../../../projects/engine/include/cheryl/core/engine/platform-dispatcher.h). diff --git a/docs/engine-systems/display.md b/docs/site/engine-systems/display.md similarity index 93% rename from docs/engine-systems/display.md rename to docs/site/engine-systems/display.md index 5404bca1..6b49673a 100644 --- a/docs/engine-systems/display.md +++ b/docs/site/engine-systems/display.md @@ -56,6 +56,8 @@ Window dimensions use logical window coordinates; framebuffer dimensions use drawable pixels and may be zero. Creation/resize requests require positive logical dimensions. Monitor inventory references are borrowed and expire on backend refresh or destruction. Copy values before deferred use. +The [current Native GLFW inventory](../runtime/display-and-window-contract.md#native-glfw-inventory-and-selection) +is captured once at construction and has no hotplug refresh. Monitor content scale is not a per-window scale notification contract. Window activation alone does not switch a renderer's graphics-resource domain. A @@ -95,6 +97,6 @@ tick in either runtime mode, so the same game remains safe when concurrent. Read [display/window contracts](../runtime/display-and-window-contract.md) for monitor freshness, mode transitions, scale and native failure limits. The -[Window](../../projects/engine/include/cheryl/core/display/window-interface.h) -and [DisplaySystem](../../projects/engine/include/cheryl/core/display/display-system-interface.h) +[Window](../../../projects/engine/include/cheryl/core/display/window-interface.h) +and [DisplaySystem](../../../projects/engine/include/cheryl/core/display/display-system-interface.h) headers define the portable interface; module details stay in the owner guide. diff --git a/docs/engine-systems/events.md b/docs/site/engine-systems/events.md similarity index 98% rename from docs/engine-systems/events.md rename to docs/site/engine-systems/events.md index 48e5c64e..7b753d75 100644 --- a/docs/engine-systems/events.md +++ b/docs/site/engine-systems/events.md @@ -105,4 +105,4 @@ Channel names/RTTI form an in-process identity, not a serialization protocol. The default bus does not provide automatic callback lifetime or UI ownership. Read [event delivery and failure contracts](../runtime/event-delivery.md), [thread dispatch](../runtime/thread-dispatch.md) and -[the bus interface](../../projects/engine/include/cheryl/core/subsystems/event-bus.h). +[the bus interface](../../../projects/engine/include/cheryl/core/subsystems/event-bus.h). diff --git a/docs/engine-systems/gui.md b/docs/site/engine-systems/gui.md similarity index 86% rename from docs/engine-systems/gui.md rename to docs/site/engine-systems/gui.md index d0990c9e..0dcc12df 100644 --- a/docs/engine-systems/gui.md +++ b/docs/site/engine-systems/gui.md @@ -30,7 +30,8 @@ their session, native authoring API and recording/publication types. | {cpp:struct}`TGUI::Capabilities `, {cpp:struct}`RmlUi::Capabilities ` | Report keyboard focus, committed text, clipboard, cursor and IME availability; RmlUi also reports clipping/effects support. [Unavailable services](../development/ui-adapters.md#routing-and-unavailable-services). | | {cpp:enum}`CE::Input::Routing::KeyboardPolicy` and pointer selection | `Exclusive`/`PassThrough` controls keyboard gameplay. The application separately selects pointer recipients. [GUI routing](../gui/index.md#route-input-and-focus-explicitly). | | {cpp:struct}`TGUI::Materials `, {cpp:struct}`RmlUi::Materials ` | Optionally override automatic defaults with compatible loaded material handles. [Custom UI materials](../rendering/pipelines-and-materials.md#custom-ui-materials). | -| {cpp:struct}`TGUI::WidgetLayout ` | Typed placement/resizing for native TGUI widgets; RmlUi uses native RML/RCSS layout. [Choose an interface](../gui/index.md#choose-an-interface); [TGUI layout declarations](../../projects/modules/ui/tgui/include/cheryl/ui/tgui/layout.h). | +| {cpp:struct}`TGUI::WidgetLayout ` | Typed placement/resizing for native TGUI widgets; RmlUi uses native RML/RCSS layout. [Choose an interface](../gui/index.md#choose-an-interface); [TGUI layout declarations](../../../projects/modules/ui/tgui/include/cheryl/ui/tgui/layout.h). | +| {cpp:class}`CE::Text::FontService` and {cpp:class}`CE::Text::Font` | Context-aware sessions inherit the Engine default and catalogue; per-widget/document overrides accept the same retained handle as direct text. [GUI fonts](../gui/index.md#select-fonts); [font service](../text/index.md). | ## Usage @@ -52,7 +53,7 @@ playback. | Types | Systems | | --- | --- | -| [`DemoGame`](../../projects/apps/demo/src/demo-game.h)
[TGUI HUD](../getting-started/create-a/hud.md)
[Application-owned UI views](../gui/index.md#construct-widgets-on-the-ui-owner) | [Rendering](rendering.md) | +| [`DemoGame`](../../../projects/apps/demo/src/demo-game.h)
[TGUI HUD](../getting-started/create-a/hud.md)
[Application-owned UI views](../gui/index.md#construct-widgets-on-the-ui-owner) | [Rendering](rendering.md) | ## Contracts @@ -86,7 +87,10 @@ state or submit owned work, not directly perform native graphics operations. ## Integration -An application creates a native button on the TGUI session owner: +With `session_` constructed on its owner as shown in +[GUI construction](../gui/index.md#construct-widgets-on-the-ui-owner), create a native +TGUI button. A session constructed with `Context` already inherits the Engine +default font and configured catalogue; no widget font setup is needed for this case: ```cpp auto button = tgui::Button::create("Apply"); @@ -108,4 +112,4 @@ There is no universal Engine widget API or context-owned UI manager. Reported capabilities define clipboard, cursor, text/focus and IME availability. Read the [module owner guides](../modules/index.md), [UI adapter contracts](../development/ui-adapters.md) and -[the maintained toolkit demo](../../projects/apps/demo/src/tgui-demo.cpp). +[the maintained toolkit demo](../../../projects/apps/demo/src/tgui-demo.cpp). diff --git a/docs/engine-systems/index.md b/docs/site/engine-systems/index.md similarity index 91% rename from docs/engine-systems/index.md rename to docs/site/engine-systems/index.md index 94604497..36d70f2b 100644 --- a/docs/engine-systems/index.md +++ b/docs/site/engine-systems/index.md @@ -5,6 +5,30 @@ application-owned game. Start here after [First Game](../getting-started/first-g when you need to understand how those boundaries fit together. The detailed topic guides remain the authority for individual operations and failure cases. +```{toctree} +:hidden: +:maxdepth: 1 +:titlesonly: + +Runtime +Display +Input +Assets +Resources +Rendering +Text +GUI +Audio
Scope and evidence

These are curated maps. Supporting fields/enums and unrelated dependencies are omitted. The C++ API reference supplies the complete declaration contracts.

All Engine arrows have declaration or implementation evidence. Application wiring is illustrative and follows the subsystem contracts.

' + current.refs.map(sourceLink).join('

') + '

Source revision ' + data.metadata.revision.slice(0, 12) + @@ -177,6 +178,13 @@ window.galleryState = { }) .join('') : '

No connections in this selected view.

'; + const operations = (node.operations || []).map(operation => + '
' + esc(operation.label) + '' + + '' + (operation.role === 'override' ? 'Implement · engine calls this hook' : 'Application calls') + + '

' + esc(operation.brief) + '

' + + (operation.api ? 'Method reference ↗
' : '') + + sourceLink(operation) + '
').join(''); $('#details').innerHTML = '

' + esc(node.label) + '

' + esc(node.kind) + '' + (node.instance ? '' + esc(node.instance) + '' : '') + @@ -188,6 +196,7 @@ window.galleryState = { '">Open API reference ↗

' : '') + sourceLink(node) + '') + + (operations ? '

Application entry points

' + operations : '') + '

Outgoing · ' + outgoing.length + '

' + list(outgoing, true) + '

Incoming · ' + incoming.length + '

' + list(incoming, false) + ''; @@ -286,7 +295,8 @@ window.galleryState = { el.dataset.key = n.key; el.setAttribute('role', 'button'); el.setAttribute('tabindex', '0'); - el.setAttribute('aria-label', n.label); + el.setAttribute('aria-label', n.label + (n.operations?.length ? '. ' + n.operations.map( + operation => (operation.role === 'override' ? 'Implement ' : '') + operation.label).join(', ') : '')); el.onclick = e => { e.stopPropagation(); if (!moved) @@ -437,6 +447,9 @@ window.galleryState = { zoom(Math.exp(-e.deltaY * .0015), e.clientX - r.left, e.clientY - r.top) }, {passive : false}); window.addEventListener('resize', fit); + const matches = (d, q) => (d.title + ' ' + d.question).toLowerCase().includes(q) || + d.nodes.some(n => n.label.toLowerCase().includes(q) || + n.operations?.some(operation => (operation.name + ' ' + operation.label).toLowerCase().includes(q))); $('#search').oninput = () => { const q = $('#search').value.toLowerCase().trim(), out = $('#search-results'); out.innerHTML = ''; @@ -445,8 +458,7 @@ window.galleryState = { if (!q) return; for (const d of data.diagrams - .filter(d => (d.title + ' ' + d.question).toLowerCase().includes(q) || - d.nodes.some(n => n.label.toLowerCase().includes(q))) + .filter(d => matches(d, q)) .slice(0, 18)) { const b = document.createElement('button'); b.innerHTML = esc(d.title) + '' + esc(viewName(d)) + ''; @@ -476,8 +488,7 @@ window.galleryState = { if (!q) return; for (const d of data.diagrams - .filter(d => (d.title + ' ' + d.question).toLowerCase().includes(q) || - d.nodes.some(n => n.label.toLowerCase().includes(q))) + .filter(d => matches(d, q)) .slice(0, 18)) { const b = document.createElement('button'); b.innerHTML = esc(d.title) + '' + esc(viewName(d)) + ''; diff --git a/docs/site/generation/static/engine-diagrams/frame.html b/docs/site/generation/static/engine-diagrams/frame.html index 4143a277..e270f7b8 100644 --- a/docs/site/generation/static/engine-diagrams/frame.html +++ b/docs/site/generation/static/engine-diagrams/frame.html @@ -1,6 +1,6 @@ Cheryl Engine subsystem diagrams -
-

100%
Drag to pan · Scroll to zoom · Click for source and connections
+
+

100%
Drag to pan · Scroll to zoom · Click for source and connections

Mermaid source

Compare views

Compare the shape and detail of each version, then open one at full size.

diff --git a/docs/site/generation/tests/test_engine_diagrams.py b/docs/site/generation/tests/test_engine_diagrams.py index 1656de49..67576d93 100644 --- a/docs/site/generation/tests/test_engine_diagrams.py +++ b/docs/site/generation/tests/test_engine_diagrams.py @@ -30,6 +30,12 @@ def fixture(): SOURCE = 'flowchart LR\n k0["Runtime::Context"]\n k1(["Application"])\n k1 --> k0\n' +def operation(): + return {"name": "CE::Runtime::Context::events", "label": "events()", "role": "call", + "file": "projects/engine/include/cheryl/core/engine/engine-context.h", "line": 129, + "brief": "Access the context-owned event bus."} + + class CatalogTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory(prefix="cheryl-diagram-tests-") @@ -70,6 +76,22 @@ def test_invalid_id_stays_inside_source(self): with self.assertRaisesRegex(ValueError, "Invalid or duplicate"): load_catalog(self.source) + def test_method_labels_must_appear_in_the_owning_box(self): + catalog = fixture() + catalog["diagrams"][0]["operations"] = {"Runtime::Context": [operation()]} + self.write(catalog) + with self.assertRaisesRegex(ValueError, "omits operation label"): + load_catalog(self.source) + self.write(catalog, SOURCE.replace('Runtime::Context"]', 'Runtime::Context
events()"]')) + self.assertEqual(load_catalog(self.source), catalog) + + def test_unknown_method_owner_is_rejected(self): + catalog = fixture() + catalog["diagrams"][0]["operations"] = {"Elsewhere": [operation()]} + self.write(catalog) + with self.assertRaisesRegex(ValueError, "Unknown operation owner"): + load_catalog(self.source) + class PublicationTests(unittest.TestCase): def test_frame_assets_change_their_browser_version(self): @@ -120,6 +142,34 @@ def test_relocatable_catalog_and_mermaid_downloads(self): self.assertEqual((destination / "runtime-application.mmd").read_text(), SOURCE) self.assertEqual(catalog, fixture(), "Compilation must not change authored metadata") + def test_methods_are_published_only_in_their_selected_view(self): + with tempfile.TemporaryDirectory(prefix="cheryl-diagram-methods-") as directory: + root = Path(directory) + source = root / "source" + source.mkdir() + catalog = fixture() + callable_operation = {**operation(), "name": "CE::Runtime::Context::operator()", "label": "operator()()"} + catalog["diagrams"][0]["operations"] = {"Runtime::Context": [operation(), callable_operation]} + other = {**fixture()["diagrams"][0], "id": "runtime-internal"} + catalog["diagrams"].append(other) + (source / "catalog.json").write_text(json.dumps(catalog)) + (source / "runtime-application.mmd").write_text( + SOURCE.replace('Runtime::Context"]', 'Runtime::Context
events()
operator()()"]')) + (source / "runtime-internal.mmd").write_text(SOURCE) + objects = [("CE::Runtime::Context::events", "", "function", "api/context", "events", 1), + ("CE::Runtime::Context::operator()", "", "function", "api/context", "invoke", 1)] + destination = root / "html" + write_catalog(source, destination, objects, lambda name: name + ".html", "https://example.com") + result = json.loads((destination / "catalog.json").read_text()) + application, internal = result["diagrams"] + method = application["nodes"][0]["operations"][0] + self.assertEqual(method["api"], "api/context.html#events") + self.assertEqual(method["brief"], operation()["brief"]) + self.assertEqual(application["nodes"][0]["operations"][1]["api"], "api/context.html#invoke") + self.assertNotIn("operations", internal["nodes"][0]) + self.assertNotIn("operations", application) + self.assertNotIn("operations", catalog["symbols"]["Runtime::Context"]) + class AnchorTests(unittest.TestCase): def hook(self): From e8b493cff57b1804306b3d95d60b9d3ff8ee91b2 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Wed, 21 Oct 2026 11:42:42 -0700 Subject: [PATCH 90/98] Clarifies action context arguments in the application input diagram Name Input::Actions::Context in Tick::actions() and ContextDefinition in ContextSubmission::attach(). Connect the existing boxes to those argument types so setup definitions and retained lookup leases are visibly distinct. Explain the ContextId alternative and borrowed View lifetime in the inspector. Checks: source/implementation review and 13 focused documentation tooling tests. --- .../site/engine-systems/diagrams/catalog.json | 41 +++++++++++++++++-- .../diagrams/input-application.mmd | 6 ++- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/docs/site/engine-systems/diagrams/catalog.json b/docs/site/engine-systems/diagrams/catalog.json index fa3ce9b6..6f398b5a 100644 --- a/docs/site/engine-systems/diagrams/catalog.json +++ b/docs/site/engine-systems/diagrams/catalog.json @@ -14879,9 +14879,42 @@ } ], "evidence_count": 1 + }, + { + "source": "Input::Actions::ContextSubmission", + "target": "Input::Actions::ContextDefinition", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/controls/action-context.h", + "line": 72, + "member": "CE::Input::Actions::ContextSubmission::attach", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "Input::Tick", + "target": "Input::Actions::Context", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/controls/tick-input.h", + "line": 74, + "member": "CE::Input::Tick::actions", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 } ], "notes": [ + "attach(ContextDefinition) returns a Context lease. Pass that lease to Tick::actions(Context) to obtain an Actions::View; the alternative overload accepts ContextId. ContextDefinition is setup data, not the lookup argument.", "Direct binding changes run during platform initialization. Reusable controllers retain a context endpoint and their own lease.", "Updates consume semantic actions and copied observations; text input/focus is a separate focused view.", "Acquire Context::input(), action_contexts() and keyboard_id() on platform during initialization; use retained endpoints and immutable tick actions afterward.", @@ -14973,10 +15006,10 @@ "Input::Actions::ContextSubmission": [ { "name": "CE::Input::Actions::ContextSubmission::attach", - "label": "attach()", + "label": "attach(Input::Actions::ContextDefinition)", "file": "projects/engine/include/cheryl/core/controls/action-context.h", "line": 72, - "brief": "Accept an owned context definition and return its lease; mappings apply at the next input poll, not immediately.", + "brief": "Accept an owned Input::Actions::ContextDefinition and return an Input::Actions::Context lease; mappings apply at the next input poll, not immediately.", "role": "call" } ], @@ -15001,10 +15034,10 @@ "Input::Tick": [ { "name": "CE::Input::Tick::actions", - "label": "actions(context)", + "label": "actions(Input::Actions::Context)", "file": "projects/engine/include/cheryl/core/controls/tick-input.h", "line": 74, - "brief": "Select one controller's context-local action namespace; the returned View borrows this update's Tick.", + "brief": "Select one controller's namespace using its Input::Actions::Context lease; the ContextId overload accepts the copied namespace identity. The returned View borrows this update's Tick.", "role": "call" }, { diff --git a/docs/site/engine-systems/diagrams/input-application.mmd b/docs/site/engine-systems/diagrams/input-application.mmd index db999ab6..dd0a71cd 100644 --- a/docs/site/engine-systems/diagrams/input-application.mmd +++ b/docs/site/engine-systems/diagrams/input-application.mmd @@ -22,11 +22,11 @@ flowchart LR k5["Input::
KeyboardKey"]:::focal k6(["Application-owned controller"]):::app k7["Input::Actions::
ContextDefinition"]:::focal - k8["Input::Actions::
ContextSubmission


attach()"]:::focal + k8["Input::Actions::
ContextSubmission


attach(Input::Actions::ContextDefinition)"]:::focal k9["Input::Actions::
Context


id()
reset()"]:::focal k10(["Application
update()"]):::app k11["Game::
TickContext"]:::neighbor - k12["Input::
Tick


actions(context)
button()
axis()"]:::focal + k12["Input::
Tick


actions(Input::Actions::Context)
button()
axis()"]:::focal k13["Input::Actions::
View


button()
axis()"]:::focal k2 --> k3 k3 --> k4 @@ -41,6 +41,8 @@ flowchart LR k6 --> k13 k0 --> k1 k1 --> k2 + k8 --> k7 + k12 --> k9 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 From 02eefc5eeb30989411d8accbbb5ab2ed94c95d66 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Wed, 21 Oct 2026 11:52:54 -0700 Subject: [PATCH 91/98] Connects application methods to their argument and result types Audit selected public operations across all 17 application diagrams. Add missing signature dependencies among shown types and declaration evidence for the existing links. Show distinct worker policy, logging backend statistics, audio publisher and text draw-packet boundaries where the existing boxes were not the actual public argument or result type. Expose method-specific Accepts and Returns links in the inspector, including free operations and inherited declarations. Explain the supporting method when an arrow is selected. Keep consumer-to-dependency arrow direction, unlabeled arrows and selective diagram scope. Validation: public declaration review across every application view, 13 focused documentation tooling tests and JavaScript syntax checks. Rendered-site checks follow for this committed source revision. --- docs/site/development/documentation.md | 9 + docs/site/engine-systems/diagram-explorer.md | 7 + .../diagrams/audio-application.mmd | 6 +- .../site/engine-systems/diagrams/catalog.json | 837 ++++++++++++++++-- .../diagrams/diagnostics-application.mmd | 2 + .../diagrams/events-application.mmd | 3 + .../diagrams/gui-application.mmd | 4 + .../diagrams/input-application.mmd | 1 + .../diagrams/logging-application.mmd | 5 + .../diagrams/resources-application.mmd | 1 + .../diagrams/runtime-application.mmd | 3 + .../diagrams/state-application.mmd | 1 + .../diagrams/text-application.mmd | 2 + .../diagrams/workers-application.mmd | 3 + .../static/engine-diagrams/explorer.css | 12 + .../static/engine-diagrams/explorer.mjs | 28 +- 16 files changed, 851 insertions(+), 73 deletions(-) diff --git a/docs/site/development/documentation.md b/docs/site/development/documentation.md index f061dca5..3eb81e54 100644 --- a/docs/site/development/documentation.md +++ b/docs/site/development/documentation.md @@ -193,6 +193,15 @@ distinguish engine callbacks from application calls. The explorer adds method search, source links and unambiguous API links without changing the type's identity or adding operation details to the internal and focused views. +Connect selected methods to their public Engine argument and result types when +those types are shown in the application view. Record signature evidence with +the exact selected declaration name in `member` and `kind: accepts` or +`kind: returns`. The explorer uses these records for each method's **Accepts** +and **Returns** links and the arrow's method explanation. Use explicit parameter +types in a method label when a short name could refer to multiple shown types. +Add another type box only when it clarifies an essential application boundary; +the views remain selective rather than duplicating the complete API inventory. + ### Incremental rebuilds and full regeneration The first generation performs the complete extraction and site build. Existing diff --git a/docs/site/engine-systems/diagram-explorer.md b/docs/site/engine-systems/diagram-explorer.md index 6513a61a..2a68be01 100644 --- a/docs/site/engine-systems/diagram-explorer.md +++ b/docs/site/engine-systems/diagram-explorer.md @@ -11,6 +11,13 @@ sequence. **Implement hooks** marks methods your game overrides for the engine to call. Select a box for each method's purpose, source and available API link; search also finds method names. +Application arrows include the argument and result types used by selected +methods. In the inspector, **Accepts** and **Returns** connect a method to those +types in the current view. Select an arrow to see the supporting method +relationship. Both use the same consumer-to-dependency direction; these arrows +do not describe data flow. The API reference supplies the complete signature, +including types omitted from the selected view. + Use **Docs navigation** to keep the documentation sidebar, or **Wide explorer** for a subsystem sidebar and more room to inspect a diagram. The selected view is preserved when changing layouts or returning from a guide. diff --git a/docs/site/engine-systems/diagrams/audio-application.mmd b/docs/site/engine-systems/diagrams/audio-application.mmd index 8f49aa2b..50dbf281 100644 --- a/docs/site/engine-systems/diagrams/audio-application.mmd +++ b/docs/site/engine-systems/diagrams/audio-application.mmd @@ -25,12 +25,13 @@ flowchart TB k8["Audio::
Voice


snapshot()
pause()
resume()
stop()"]:::focal k9["Audio::
VoiceSnapshot"]:::focal k10(["Application
listener setup"]):::app - k11["Events::
EventBus


register_listener()"]:::neighbor + k11["Events::
EventBus


register_listener()
publisher()"]:::neighbor k12["Audio::
PlaybackEnded"]:::focal k13(["Application
init_platform()"]):::app k14["Runtime::
RuntimeServices"]:::neighbor k15["Runtime::
ServiceAttachment


reset()"]:::neighbor k16(["Application
cleanup / quiesce()"]):::app + k17["Events::EventBus::
Publisher"]:::neighbor k0 --> k1 k2 -.-> k4 k5 --> k6 @@ -48,6 +49,9 @@ flowchart TB k16 --> k6 k2 --> k3 k3 --> k4 + k6 --> k7 + k11 --> k17 + k1 --> k17 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 diff --git a/docs/site/engine-systems/diagrams/catalog.json b/docs/site/engine-systems/diagrams/catalog.json index 6f398b5a..e541124e 100644 --- a/docs/site/engine-systems/diagrams/catalog.json +++ b/docs/site/engine-systems/diagrams/catalog.json @@ -2122,9 +2122,9 @@ "label": "Rendering::Submission::resolve_text()", "name": "CE::Rendering::Submission::resolve_text", "kind": "operation", - "brief": "Lay out legacy text on the CPU and resolve ordered six-vertex glyph packets.", + "brief": "Resolve retained text generations or legacy-font layout into retained CPU draw packets.", "file": "projects/engine/include/cheryl/assets/submission/draw2d.h", - "line": 62, + "line": 70, "key": "Rendering::Submission::resolve_text()", "system": "rendering", "instance": "" @@ -2594,6 +2594,30 @@ "key": "Audio::Miniaudio::decode_file()", "system": "audio", "instance": "" + }, + "Workers::GroupPolicy": { + "name": "CE::Workers::GroupPolicy", + "label": "Workers::GroupPolicy", + "kind": "struct", + "file": "projects/engine/include/cheryl/core/engine/worker-pool.h", + "line": 91, + "brief": "Immutable resolved policy reported at group creation, distinct from per-job native observations.", + "symbol": "Workers::GroupPolicy", + "key": "Workers::GroupPolicy", + "system": "workers", + "instance": "" + }, + "Logging::BackendStats": { + "name": "CE::Logging::BackendStats", + "label": "Logging::BackendStats", + "kind": "struct", + "file": "projects/engine/include/cheryl/core/logging/backend-guard.h", + "line": 12, + "brief": "Cumulative observations for one Log, including failed openings and all generations.", + "symbol": "Logging::BackendStats", + "key": "Logging::BackendStats", + "system": "logging", + "instance": "" } }, "diagrams": [ @@ -13403,9 +13427,17 @@ "line": 35, "member": "Application::GLFWOpenGL::make_context()", "via": "operation signature" + }, + { + "kind": "returns", + "file": "projects/modules/graphics/opengl/include/cheryl/backends/opengl/glfw-backend.h", + "line": 33, + "member": "CE::Application::GLFWOpenGL::make_context", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 2 + "evidence_count": 3 }, { "source": "@host", @@ -13762,12 +13794,59 @@ } ], "evidence_count": 1 + }, + { + "source": "Runtime::GameRuntime", + "target": "Runtime::ShutdownPolicy", + "optional": false, + "evidence": [ + { + "kind": "returns", + "file": "projects/engine/include/cheryl/core/game-framework/game-runtime.h", + "line": 117, + "member": "CE::Runtime::GameRuntime::set_shutdown_policy", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "Game::AbstractGame", + "target": "Game::TickContext", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/game-framework/abstract-game.h", + "line": 33, + "member": "CE::Game::AbstractGame::update", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "Game::AbstractGame", + "target": "Rendering::FrameWriter", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/game-framework/abstract-game.h", + "line": 34, + "member": "CE::Game::AbstractGame::prepare_render_frame", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 } ], "notes": [ "The composition role owns Context, the game and GameRuntime. The game supplies init/update/frame/quiesce/deinit hooks.", - "Use the supplied compatible factory for the normal native/OpenGL assembly; manual adapters are shown in the focused composition view.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Use the supplied compatible factory for the normal native/OpenGL assembly; manual adapters are shown in the focused composition view." ], "level": "detailed", "refs": [ @@ -14310,15 +14389,22 @@ "member": "CE::Rendering::Camera2D::recalculate_projection", "access": "invalid", "via": "libclang AST" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/rendering/camera.h", + "line": 22, + "member": "CE::Rendering::CameraBase::set_framebuffer_size", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 2 + "evidence_count": 3 } ], "notes": [ "Direct window and monitor operations require the platform owner; cross-owner requests use a saved platform endpoint.", - "Use copied tick dimensions for simulation layout. Monitor content scale and framebuffer pixels are distinct observations.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Use copied tick dimensions for simulation layout. Monitor content scale and framebuffer pixels are distinct observations." ], "level": "detailed", "refs": [ @@ -14911,14 +14997,29 @@ } ], "evidence_count": 1 + }, + { + "source": "Input::System", + "target": "Input::Actions::ContextSubmission", + "optional": false, + "evidence": [ + { + "kind": "returns", + "file": "projects/engine/include/cheryl/core/controls/input-interface.h", + "line": 38, + "member": "CE::Input::System::action_contexts", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 } ], "notes": [ "attach(ContextDefinition) returns a Context lease. Pass that lease to Tick::actions(Context) to obtain an Actions::View; the alternative overload accepts ContextId. ContextDefinition is setup data, not the lookup argument.", "Direct binding changes run during platform initialization. Reusable controllers retain a context endpoint and their own lease.", "Updates consume semantic actions and copied observations; text input/focus is a separate focused view.", - "Acquire Context::input(), action_contexts() and keyboard_id() on platform during initialization; use retained endpoints and immutable tick actions afterward.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Acquire Context::input(), action_contexts() and keyboard_id() on platform during initialization; use retained endpoints and immutable tick actions afterward." ], "level": "detailed", "refs": [ @@ -15334,9 +15435,17 @@ "line": 39, "member": "Rendering::Submission::resolve_sprite()", "via": "operation signature" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/assets/submission/draw2d.h", + "line": 39, + "member": "CE::Rendering::Submission::resolve_sprite", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Rendering::Submission::resolve_sprite()", @@ -15348,9 +15457,17 @@ "line": 39, "member": "Rendering::Submission::resolve_sprite()", "via": "operation signature" + }, + { + "kind": "returns", + "file": "projects/engine/include/cheryl/assets/submission/draw2d.h", + "line": 39, + "member": "CE::Rendering::Submission::resolve_sprite", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Assets::Sprites::Manager", @@ -15403,8 +15520,7 @@ ], "notes": [ "Shared asset metadata stays immutable; application objects own transforms, gameplay and playback state.", - "A failed upload may leave earlier successful entries published. Validate looked-up handles and release game copies before backend closure.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "A failed upload may leave earlier successful entries published. Validate looked-up handles and release game copies before backend closure." ], "level": "detailed", "refs": [ @@ -15565,9 +15681,25 @@ "line": 24, "member": "Assets::Images::decode()", "via": "operation signature" + }, + { + "kind": "returns", + "file": "projects/engine/include/cheryl/assets/resources/decoded-image.h", + "line": 22, + "member": "CE::Assets::Images::decode", + "access": "public", + "via": "member declaration" + }, + { + "kind": "returns", + "file": "projects/engine/include/cheryl/assets/resources/decoded-image.h", + "line": 24, + "member": "CE::Assets::Images::decode", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 2 + "evidence_count": 4 }, { "source": "@init", @@ -15952,12 +16084,27 @@ } ], "evidence_count": 1 + }, + { + "source": "Rendering::Resources::Provider", + "target": "Rendering::GraphicsDomain", + "optional": false, + "evidence": [ + { + "kind": "returns", + "file": "projects/engine/include/cheryl/assets/resources/resource-provider.h", + "line": 59, + "member": "CE::Rendering::Resources::Provider::graphics_domain", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 } ], "notes": [ "Decode may run on CPU workers; native upload must use the initialized provider/loading owner.", - "Ordinary sprite/text/UI loading supplies default materials. Direct pipeline/material construction is an advanced option, not a prerequisite.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Ordinary sprite/text/UI loading supplies default materials. Direct pipeline/material construction is an advanced option, not a prerequisite." ], "level": "detailed", "refs": [ @@ -16158,9 +16305,17 @@ "line": 39, "member": "Rendering::Submission::resolve_sprite()", "via": "operation signature" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/assets/submission/draw2d.h", + "line": 39, + "member": "CE::Rendering::Submission::resolve_sprite", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Rendering::Submission::resolve_sprite()", @@ -16172,9 +16327,17 @@ "line": 39, "member": "Rendering::Submission::resolve_sprite()", "via": "operation signature" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/assets/submission/draw2d.h", + "line": 39, + "member": "CE::Rendering::Submission::resolve_sprite", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Rendering::Submission::resolve_sprite()", @@ -16186,9 +16349,17 @@ "line": 39, "member": "Rendering::Submission::resolve_sprite()", "via": "operation signature" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/assets/submission/draw2d.h", + "line": 39, + "member": "CE::Rendering::Submission::resolve_sprite", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Rendering::Submission::resolve_sprite()", @@ -16200,9 +16371,17 @@ "line": 39, "member": "Rendering::Submission::resolve_sprite()", "via": "operation signature" + }, + { + "kind": "returns", + "file": "projects/engine/include/cheryl/assets/submission/draw2d.h", + "line": 39, + "member": "CE::Rendering::Submission::resolve_sprite", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Rendering::PassWriter", @@ -16279,8 +16458,7 @@ ], "notes": [ "Advance camera/gameplay/animation in update. Preparation may be skipped; it only snapshots the chosen appearance.", - "Writers borrow one runtime slot for the hook. Native draw/upload work belongs to graphics; loaded assets already supply ordinary materials.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Writers borrow one runtime slot for the hook. Native draw/upload work belongs to graphics; loaded assets already supply ordinary materials." ], "level": "detailed", "refs": [ @@ -16552,9 +16730,17 @@ "line": 84, "member": "Text::layout_text()", "via": "operation signature" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/text/layout.h", + "line": 84, + "member": "CE::Text::layout_text", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Text::layout_text()", @@ -16572,9 +16758,25 @@ "line": 84, "member": "Text::layout_text()", "via": "operation signature" + }, + { + "kind": "returns", + "file": "projects/engine/include/cheryl/text/layout.h", + "line": 83, + "member": "CE::Text::layout_text", + "access": "public", + "via": "member declaration" + }, + { + "kind": "returns", + "file": "projects/engine/include/cheryl/text/layout.h", + "line": 84, + "member": "CE::Text::layout_text", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 2 + "evidence_count": 4 }, { "source": "@load", @@ -16598,9 +16800,17 @@ "line": 115, "member": "Text::Rendering::prepare()", "via": "operation signature" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/assets/types/2d/unicode-text.h", + "line": 115, + "member": "CE::Text::Rendering::prepare", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Text::Rendering::prepare()", @@ -16612,9 +16822,17 @@ "line": 115, "member": "Text::Rendering::prepare()", "via": "operation signature" + }, + { + "kind": "returns", + "file": "projects/engine/include/cheryl/assets/types/2d/unicode-text.h", + "line": 115, + "member": "CE::Text::Rendering::prepare", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "@init", @@ -16638,9 +16856,17 @@ "line": 116, "member": "Text::Rendering::upload()", "via": "operation signature" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/assets/types/2d/unicode-text.h", + "line": 116, + "member": "CE::Text::Rendering::upload", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Text::Rendering::upload()", @@ -16652,9 +16878,17 @@ "line": 116, "member": "Text::Rendering::upload()", "via": "operation signature" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/assets/types/2d/unicode-text.h", + "line": 116, + "member": "CE::Text::Rendering::upload", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Text::Rendering::upload()", @@ -16666,9 +16900,17 @@ "line": 116, "member": "Text::Rendering::upload()", "via": "operation signature" + }, + { + "kind": "returns", + "file": "projects/engine/include/cheryl/assets/types/2d/unicode-text.h", + "line": 116, + "member": "CE::Text::Rendering::upload", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "@update", @@ -16848,15 +17090,38 @@ "line": 70, "member": "Rendering::Submission::resolve_text()", "via": "operation signature" - } - ], - "evidence_count": 1 - } - ], + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/assets/submission/draw2d.h", + "line": 70, + "member": "CE::Rendering::Submission::resolve_text", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 2 + }, + { + "source": "Rendering::Submission::resolve_text()", + "target": "Rendering::DrawPacket2D", + "optional": false, + "evidence": [ + { + "kind": "returns", + "file": "projects/engine/include/cheryl/assets/submission/draw2d.h", + "line": 70, + "member": "CE::Rendering::Submission::resolve_text", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + } + ], "notes": [ "Static labels explicitly layout, prepare and upload after graphics initialization. Changing labels can use Updater instead of application futures.", - "Context supplies an embedded default font. Configure named families/defaults before consumers; changing the catalogue does not rebind existing handles.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Context supplies an embedded default font. Configure named families/defaults before consumers; changing the catalogue does not rebind existing handles." ], "level": "detailed", "refs": [ @@ -16880,7 +17145,8 @@ "Text::Rendering::Updater", "Text::Rendering::Request", "@prepare", - "Rendering::Submission::resolve_text()" + "Rendering::Submission::resolve_text()", + "Rendering::DrawPacket2D" ], "operations": { "Runtime::Context": [ @@ -17390,9 +17656,33 @@ "member": "CE::UI::TGUI::SceneUploader::upload", "access": "public", "via": "member declaration" + }, + { + "kind": "accepts", + "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/scene.h", + "line": 61, + "member": "CE::UI::TGUI::SceneUploader::upload", + "access": "public", + "via": "member declaration" + }, + { + "kind": "accepts", + "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/scene.h", + "line": 63, + "member": "CE::UI::TGUI::SceneUploader::submit", + "access": "public", + "via": "member declaration" + }, + { + "kind": "accepts", + "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/scene.h", + "line": 65, + "member": "CE::UI::TGUI::SceneUploader::submit", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 4 }, { "source": "@ui", @@ -17418,9 +17708,33 @@ "member": "CE::UI::RmlUi::SceneUploader::upload", "access": "public", "via": "member declaration" + }, + { + "kind": "accepts", + "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/scene.h", + "line": 56, + "member": "CE::UI::RmlUi::SceneUploader::upload", + "access": "public", + "via": "member declaration" + }, + { + "kind": "accepts", + "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/scene.h", + "line": 58, + "member": "CE::UI::RmlUi::SceneUploader::submit", + "access": "public", + "via": "member declaration" + }, + { + "kind": "accepts", + "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/scene.h", + "line": 60, + "member": "CE::UI::RmlUi::SceneUploader::submit", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 4 }, { "source": "@ui", @@ -17433,13 +17747,92 @@ } ], "evidence_count": 1 + }, + { + "source": "UI::TGUI::Session", + "target": "Input::Events::Record", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/session.h", + "line": 95, + "member": "CE::UI::TGUI::Session::handle_input", + "access": "public", + "via": "member declaration" + }, + { + "kind": "accepts", + "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/session.h", + "line": 99, + "member": "CE::UI::TGUI::Session::handle_input", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 2 + }, + { + "source": "UI::TGUI::Session", + "target": "Input::Routing::KeyboardPolicy", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/session.h", + "line": 89, + "member": "CE::UI::TGUI::Session::request_keyboard_focus", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "UI::RmlUi::Session", + "target": "Input::Events::Record", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/session.h", + "line": 93, + "member": "CE::UI::RmlUi::Session::handle_input", + "access": "public", + "via": "member declaration" + }, + { + "kind": "accepts", + "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/session.h", + "line": 97, + "member": "CE::UI::RmlUi::Session::handle_input", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 2 + }, + { + "source": "UI::RmlUi::Session", + "target": "Input::Routing::KeyboardPolicy", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/session.h", + "line": 90, + "member": "CE::UI::RmlUi::Session::request_keyboard_focus", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 } ], "notes": [ "Dashed choices are alternatives: select a toolkit, do not infer a shared Engine widget API. Its widgets/documents remain application-owned.", "Application code feeds the complete input batch, selects pointer delivery, requests focus and publishes recordings. Quiesce UI calls before session closure.", - "Sessions need view and time updates. Enable automatic_view/automatic_time when constructing a Context-aware session, or call set_view()/update_time() explicitly; both options default off.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Sessions need view and time updates. Enable automatic_view/automatic_time when constructing a Context-aware session, or call set_view()/update_time() explicitly; both options default off." ], "level": "detailed", "refs": [ @@ -17975,13 +18368,68 @@ } ], "evidence_count": 1 + }, + { + "source": "Audio::System", + "target": "Audio::PlaybackOptions", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/audio/system.h", + "line": 75, + "member": "CE::Audio::System::play", + "access": "public", + "via": "member declaration" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/audio/system.h", + "line": 77, + "member": "CE::Audio::System::stream", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 2 + }, + { + "source": "Events::EventBus", + "target": "Events::EventBus::Publisher", + "optional": false, + "evidence": [ + { + "kind": "returns", + "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", + "line": 184, + "member": "CE::Events::EventBus::publisher", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "Audio::Miniaudio::System", + "target": "Events::EventBus::Publisher", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/modules/audio/miniaudio/include/cheryl/backends/miniaudio.h", + "line": 56, + "member": "CE::Audio::Miniaudio::System::publish_events_to", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 } ], "notes": [ "Output lifetime is independent of Context. Keep the source file available for streaming, and close output after its producers settle.", "Dropping a Voice handle does not stop playback. maintain()/notification pumping is explicit and does not advance the audio clock.", - "decode_file() is the ordinary whole-file loading path. Clip::from_samples() is the alternative for application-supplied PCM; stream() keeps its source file available.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "decode_file() is the ordinary whole-file loading path. Clip::from_samples() is the alternative for application-supplied PCM; stream() keeps its source file available." ], "level": "detailed", "refs": [ @@ -18004,7 +18452,8 @@ "@init", "Runtime::RuntimeServices", "Runtime::ServiceAttachment", - "@cleanup" + "@cleanup", + "Events::EventBus::Publisher" ], "operations": { "Audio::Miniaudio::System": [ @@ -18127,6 +18576,14 @@ "line": 206, "brief": "Register a persistent typed PlaybackEnded listener. Immediate delivery runs on the notification-pump owner; use an explicit delivery adapter for simulation-owned state.", "role": "call" + }, + { + "name": "CE::Events::EventBus::publisher", + "label": "publisher()", + "role": "call", + "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", + "line": 184, + "brief": "Acquire the weak notification endpoint to pass to publish_events_to(); it does not retain the bus." } ], "Runtime::ServiceAttachment": [ @@ -18244,9 +18701,17 @@ "member": "CE::Events::EventBus::notify", "access": "invalid", "via": "libclang AST" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", + "line": 207, + "member": "CE::Events::EventBus::register_listener", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 7 + "evidence_count": 8 }, { "source": "Events::EventBus", @@ -18354,9 +18819,17 @@ "line": 16, "member": "Events::simulation_delivery()", "via": "operation signature" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/engine/event-delivery.h", + "line": 16, + "member": "CE::Events::simulation_delivery", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Events::simulation_delivery()", @@ -18368,9 +18841,17 @@ "line": 16, "member": "Events::simulation_delivery()", "via": "operation signature" + }, + { + "kind": "returns", + "file": "projects/engine/include/cheryl/core/engine/event-delivery.h", + "line": 16, + "member": "CE::Events::simulation_delivery", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "Events::EventBus", @@ -18394,9 +18875,17 @@ "line": 260, "member": "Events::EventBus::register_channel", "via": "API declaration" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", + "line": 209, + "member": "CE::Events::EventBus::register_listener", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 3 + "evidence_count": 4 }, { "source": "@listener", @@ -18481,13 +18970,60 @@ } ], "evidence_count": 1 + }, + { + "source": "Events::EventBus::Publisher", + "target": "Events::EventChannel", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", + "line": 168, + "member": "CE::Events::EventBus::Publisher::notify", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "Events::EventBus::Publisher", + "target": "Events::EventBus::ErrorHandler", + "optional": true, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", + "line": 170, + "member": "CE::Events::EventBus::Publisher::notify", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "Events::EventBus", + "target": "Events::EventBus::ErrorHandler", + "optional": true, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", + "line": 210, + "member": "CE::Events::EventBus::register_listener", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 } ], "notes": [ "Immediate callbacks run on the producer. Explicit delivery selects a different owner and requires an independent error sink.", "Dropping Registration does not unregister. Invalidate and settle entered callbacks before destroying borrowed targets.", - "Context::events() is the normal route for runtime-connected engine observations. An application-owned bus is also valid; the selected producer determines which bus to subscribe to. Registration::disconnect() invalidates entry without waiting; unregister_and_wait() additionally settles entered callbacks, except self-wait rejects.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Context::events() is the normal route for runtime-connected engine observations. An application-owned bus is also valid; the selected producer determines which bus to subscribe to. Registration::disconnect() invalidates entry without waiting; unregister_and_wait() additionally settles entered callbacks, except self-wait rejects." ], "level": "detailed", "refs": [ @@ -18929,8 +19465,7 @@ ], "notes": [ "Save copyable endpoints during initialization. Platform work receives Context; simulation requests drain at update boundaries.", - "Read retained futures only when ready. Never wait on the owner required to finish the request; endpoint lifetime does not extend borrowed captures.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Read retained futures only when ready. Never wait on the owner required to finish the request; endpoint lifetime does not extend borrowed captures." ], "level": "detailed", "refs": [ @@ -19326,12 +19861,59 @@ } ], "evidence_count": 1 + }, + { + "source": "Workers::Pool", + "target": "Workers::Group", + "optional": false, + "evidence": [ + { + "kind": "returns", + "file": "projects/engine/include/cheryl/core/engine/worker-pool.h", + "line": 200, + "member": "CE::Workers::Pool::make_group", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "Workers::Pool", + "target": "Workers::GroupOptions", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/engine/worker-pool.h", + "line": 200, + "member": "CE::Workers::Pool::make_group", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "Runtime::Context", + "target": "Workers::GroupOptions", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/engine/engine-context.h", + "line": 124, + "member": "CE::Runtime::Context::make_worker_group", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 } ], "notes": [ "The preparation role supplies its own owned job inputs and may post a graphics follow-up. That dependency belongs to the job, not Pool.", - "A group cap does not create more physical workers. Context closes its tracked groups; an injected pool can continue serving unrelated groups.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "A group cap does not create more physical workers. Context closes its tracked groups; an injected pool can continue serving unrelated groups." ], "level": "detailed", "refs": [ @@ -19511,8 +20093,7 @@ ], "notes": [ "Start with pooled retained handles. Reservations are for explicitly selected construction with an allocator that satisfies the release contract.", - "Final handles retain release state; this neither synchronizes object contents nor authorizes use of a destroyed pool facade.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Final handles retain release state; this neither synchronizes object contents nor authorizes use of a destroyed pool facade." ], "level": "detailed", "refs": [ @@ -19729,12 +20310,27 @@ } ], "evidence_count": 1 + }, + { + "source": "State::ObservedVariable", + "target": "State::VersionedSnapshot", + "optional": false, + "evidence": [ + { + "kind": "returns", + "file": "projects/engine/include/cheryl/templates/observed-variables.h", + "line": 50, + "member": "CE::State::ObservedVariable::snapshot", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 } ], "notes": [ "These are alternatives, not mandatory layers. Trackers/machines need caller synchronization; a versioned snapshot copies value and revision together.", - "Observers run on setter threads. Publication coalesces latest values and is not an event history or owner assignment.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Observers run on setter threads. Publication coalesces latest values and is not an event history or owner assignment." ], "level": "detailed", "refs": [ @@ -20269,12 +20865,75 @@ } ], "evidence_count": 1 + }, + { + "source": "Logging::Log", + "target": "Logging::Config", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/logging/log.h", + "line": 119, + "member": "CE::Logging::Log::Log", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "Logging::Logger", + "target": "Logging::FileEventHandlers", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/logging/logger.h", + "line": 51, + "member": "CE::Logging::Logger::initialize", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "Logging::Logger", + "target": "Logging::Config", + "optional": false, + "evidence": [ + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/core/logging/logger.h", + "line": 51, + "member": "CE::Logging::Logger::initialize", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 + }, + { + "source": "Logging::LogHandle", + "target": "Logging::BackendStats", + "optional": false, + "evidence": [ + { + "kind": "returns", + "file": "projects/engine/include/cheryl/core/logging/log.h", + "line": 68, + "member": "CE::Logging::LogHandle::backend_stats", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 } ], "notes": [ "Configure used categories before writers; stop/settle producers before flushing and closing. Keep file callback targets alive through final closure.", - "Runtime thresholds cannot restore stripped calls. Logging failure handling and required operation failures remain separate.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Runtime thresholds cannot restore stripped calls. Logging failure handling and required operation failures remain separate." ], "level": "detailed", "refs": [ @@ -20294,7 +20953,8 @@ "@report", "Logging::LogHandle", "Logging::QueueStats", - "@cleanup" + "@cleanup", + "Logging::BackendStats" ], "operations": { "Logging::ProcessLifetime": [ @@ -20755,12 +21415,27 @@ } ], "evidence_count": 1 + }, + { + "source": "Workers::Group", + "target": "Workers::GroupPolicy", + "optional": false, + "evidence": [ + { + "kind": "returns", + "file": "projects/engine/include/cheryl/core/engine/worker-pool.h", + "line": 169, + "member": "CE::Workers::Group::policy", + "access": "public", + "via": "member declaration" + } + ], + "evidence_count": 1 } ], "notes": [ "Query GameRuntime::diagnostics() before run or after synchronizing with its return/throw, never as a live runtime sampler.", - "Other snapshots have owner-specific rules. Empty queues/counters do not prove completion, budgets or hardware behavior.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Other snapshots have owner-specific rules. Empty queues/counters do not prove completion, budgets or hardware behavior." ], "level": "detailed", "refs": [ @@ -20777,7 +21452,8 @@ "Diagnostics::DispatchStats", "Events::EventBus", "Events::EventStats", - "Logging::Logger" + "Logging::Logger", + "Workers::GroupPolicy" ], "operations": { "Runtime::GameRuntime": [ @@ -20905,9 +21581,17 @@ "line": 19, "member": "Memory::adjust_length()", "via": "operation signature" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/math/fit.h", + "line": 19, + "member": "CE::Memory::adjust_length", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 2 }, { "source": "@placement", @@ -20931,9 +21615,25 @@ "via": "public geometry operation argument", "line": 47, "member": "CE::Rendering::Geometry::Anchor::MakeQuad" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/math/anchor.h", + "line": 46, + "member": "CE::Rendering::Geometry::Anchor::MakeQuad", + "access": "public", + "via": "member declaration" + }, + { + "kind": "accepts", + "file": "projects/engine/include/cheryl/math/anchor.h", + "line": 55, + "member": "CE::Rendering::Geometry::Anchor::MakeQuadStrip", + "access": "public", + "via": "member declaration" } ], - "evidence_count": 1 + "evidence_count": 3 }, { "source": "@config", @@ -20966,8 +21666,7 @@ ], "notes": [ "File discovery can block and its results do not refresh automatically. Bit proxies and borrowed index views retain their own lifetime/synchronization rules.", - "Use tick.delta_seconds for game simulation; DeltaTime measures an independent local elapsed interval.", - "Methods beneath type names are selected application entry points, not complete inventories. Implement hooks marks engine-invoked overrides. Select a box for method contracts and declaration links." + "Use tick.delta_seconds for game simulation; DeltaTime measures an independent local elapsed interval." ], "level": "detailed", "refs": [ diff --git a/docs/site/engine-systems/diagrams/diagnostics-application.mmd b/docs/site/engine-systems/diagrams/diagnostics-application.mmd index ff0bf4dd..4d5b2521 100644 --- a/docs/site/engine-systems/diagrams/diagnostics-application.mmd +++ b/docs/site/engine-systems/diagrams/diagnostics-application.mmd @@ -25,6 +25,7 @@ flowchart TB k8["Events::
EventBus


diagnostics()"]:::neighbor k9["Events::
EventStats"]:::focal k10["Logging::
Logger


info()"]:::neighbor + k11["Workers::
GroupPolicy"]:::focal k0 --> k1 k1 --> k2 k0 --> k2 @@ -35,6 +36,7 @@ flowchart TB k3 --> k8 k8 --> k9 k0 -.-> k10 + k4 --> k11 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 diff --git a/docs/site/engine-systems/diagrams/events-application.mmd b/docs/site/engine-systems/diagrams/events-application.mmd index e8f1103c..7ad31bc5 100644 --- a/docs/site/engine-systems/diagrams/events-application.mmd +++ b/docs/site/engine-systems/diagrams/events-application.mmd @@ -43,6 +43,9 @@ flowchart LR k4 --> k5 k5 --> k2 k12 --> k5 + k2 --> k1 + k2 -.-> k11 + k5 -.-> k11 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 diff --git a/docs/site/engine-systems/diagrams/gui-application.mmd b/docs/site/engine-systems/diagrams/gui-application.mmd index 4b621f3c..73f4e459 100644 --- a/docs/site/engine-systems/diagrams/gui-application.mmd +++ b/docs/site/engine-systems/diagrams/gui-application.mmd @@ -48,6 +48,10 @@ flowchart TB k0 -.-> k9 k10 --> k7 k0 -.-> k10 + k1 --> k3 + k1 --> k4 + k2 --> k3 + k2 --> k4 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 diff --git a/docs/site/engine-systems/diagrams/input-application.mmd b/docs/site/engine-systems/diagrams/input-application.mmd index dd0a71cd..f97b5f82 100644 --- a/docs/site/engine-systems/diagrams/input-application.mmd +++ b/docs/site/engine-systems/diagrams/input-application.mmd @@ -43,6 +43,7 @@ flowchart LR k1 --> k2 k8 --> k7 k12 --> k9 + k2 --> k8 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 diff --git a/docs/site/engine-systems/diagrams/logging-application.mmd b/docs/site/engine-systems/diagrams/logging-application.mmd index e612930d..8fb33ff0 100644 --- a/docs/site/engine-systems/diagrams/logging-application.mmd +++ b/docs/site/engine-systems/diagrams/logging-application.mmd @@ -28,6 +28,7 @@ flowchart TB k11["Logging::
LogHandle


shared_queue_stats()
backend_stats()
report_diagnostics()"]:::focal k12["Logging::
QueueStats"]:::neighbor k13(["Application
cleanup / quiesce()"]):::app + k14["Logging::
BackendStats"]:::neighbor k0 --> k1 k0 --> k2 k0 --> k3 @@ -41,6 +42,10 @@ flowchart TB k11 --> k12 k13 --> k3 k0 --> k9 + k3 --> k2 + k9 --> k7 + k9 --> k2 + k11 --> k14 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 diff --git a/docs/site/engine-systems/diagrams/resources-application.mmd b/docs/site/engine-systems/diagrams/resources-application.mmd index 5e274bce..cd642b28 100644 --- a/docs/site/engine-systems/diagrams/resources-application.mmd +++ b/docs/site/engine-systems/diagrams/resources-application.mmd @@ -42,6 +42,7 @@ flowchart TB k12 --> k9 k12 --> k8 k13 --> k14 + k5 --> k14 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 diff --git a/docs/site/engine-systems/diagrams/runtime-application.mmd b/docs/site/engine-systems/diagrams/runtime-application.mmd index b5c91f82..540cf849 100644 --- a/docs/site/engine-systems/diagrams/runtime-application.mmd +++ b/docs/site/engine-systems/diagrams/runtime-application.mmd @@ -42,6 +42,9 @@ flowchart TB k3 --> k12 k3 --> k13 k14 --> k13 + k3 --> k10 + k5 --> k7 + k5 --> k9 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 diff --git a/docs/site/engine-systems/diagrams/state-application.mmd b/docs/site/engine-systems/diagrams/state-application.mmd index 14d2bae8..150a53a0 100644 --- a/docs/site/engine-systems/diagrams/state-application.mmd +++ b/docs/site/engine-systems/diagrams/state-application.mmd @@ -33,6 +33,7 @@ flowchart TB k2 --> k8 k7 --> k4 k7 -.-> k5 + k5 --> k6 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 diff --git a/docs/site/engine-systems/diagrams/text-application.mmd b/docs/site/engine-systems/diagrams/text-application.mmd index bf9344b6..fda75d4e 100644 --- a/docs/site/engine-systems/diagrams/text-application.mmd +++ b/docs/site/engine-systems/diagrams/text-application.mmd @@ -32,6 +32,7 @@ flowchart TB k15["Text::Rendering::
Request"]:::focal k16(["Application
prepare_render_frame()"]):::app k17("Rendering::Submission::
resolve_text()"):::neighbor + k18["Rendering::
DrawPacket2D"]:::neighbor k0 --> k1 k1 --> k2 k3 --> k4 @@ -51,6 +52,7 @@ flowchart TB k14 --> k12 k16 --> k17 k17 --> k12 + k17 --> k18 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 diff --git a/docs/site/engine-systems/diagrams/workers-application.mmd b/docs/site/engine-systems/diagrams/workers-application.mmd index 25f79810..b060a9f4 100644 --- a/docs/site/engine-systems/diagrams/workers-application.mmd +++ b/docs/site/engine-systems/diagrams/workers-application.mmd @@ -36,6 +36,9 @@ flowchart LR k5 --> k8 k5 --> k9 k10 --> k4 + k2 --> k4 + k2 --> k6 + k3 --> k6 classDef focal fill:#e7f3ef,stroke:#387d6b,color:#153d33,stroke-width:1.2px classDef neighbor fill:#f0f3f8,stroke:#8c9bb1,color:#35445b,stroke-width:1px classDef app fill:#fff2df,stroke:#c09452,color:#674515,stroke-dasharray:5 3 diff --git a/docs/site/generation/static/engine-diagrams/explorer.css b/docs/site/generation/static/engine-diagrams/explorer.css index e52ca4a8..783b6886 100644 --- a/docs/site/generation/static/engine-diagrams/explorer.css +++ b/docs/site/generation/static/engine-diagrams/explorer.css @@ -390,6 +390,18 @@ h1 span { font:11px/1.5 ui-monospace,SFMono-Regular,monospace; overflow-wrap:anywhere } +.signature-types { + display:grid; + gap:3px; + margin:8px 0; +} +.signature-types span { + font-size:10px; + color:var(--muted); +} +.signature-relation code { + overflow-wrap:anywhere; +} .return { border:1px solid #cdd9ea; border-radius:4px; diff --git a/docs/site/generation/static/engine-diagrams/explorer.mjs b/docs/site/generation/static/engine-diagrams/explorer.mjs index 4fa391bc..9f32a084 100644 --- a/docs/site/generation/static/engine-diagrams/explorer.mjs +++ b/docs/site/generation/static/engine-diagrams/explorer.mjs @@ -127,7 +127,7 @@ window.galleryState = { (current.system === 'overview' ? '

Each box is a documented responsibility. Click it to open that subsystem. Click an arrow for supporting types.

' : '
Types in this subsystem
Other Engine contracts
Illustrative application roles

Named free functions appear as operation boxes where they form the actual API boundary. Rounded cream boxes are roles, not Engine types.

' + - (current.view === 'application' ? '

Methods beneath a type name are selected application entry points. “Implement hooks” identifies methods you override for the engine to call. Select the box for method contracts and reference links.

' : '')) + + (current.view === 'application' ? '

Methods beneath a type name are selected application entry points. “Implement hooks” identifies methods you override for the engine to call. Select the box for method contracts, reference links and Accepts/Returns links to the types shown in this view.

' : '')) + '

Arrows

Consumer → dependency. No chronological order is implied. Dashed arrows mark optional or alternative choices.

Scope and evidence

These are curated maps. Supporting fields/enums and unrelated dependencies are omitted. The C++ API reference supplies the complete declaration contracts.

All Engine arrows have declaration or implementation evidence. Application wiring is illustrative and follows the subsystem contracts.

' + current.refs.map(sourceLink).join('

') + '

Source revision ' + data.metadata.revision.slice(0, 12) + @@ -139,6 +139,22 @@ window.galleryState = { .forEach(el => el.classList.remove('dim', 'selected', 'outgoing', 'incoming')); defaultDetails() } + function signatureKinds(edge, operation) { + return ['accepts', 'returns'].filter(kind => edge.evidence?.some( + ref => ref.member === operation.name && ref.kind === kind)); + } + function signatureLinks(node, operation) { + return ['accepts', 'returns'].map(kind => { + const targets = current.edges.filter(edge => edge.source === node.key && + signatureKinds(edge, operation).includes(kind)).map(edge => + current.nodes.find(target => target.key === edge.target)); + if (!targets.length) return ''; + return '

' + + (kind === 'accepts' ? 'Accepts' : 'Returns') + '' + targets.map(target => + '').join('') + '
'; + }).join(''); + } function select(key) { selected = key; const node = current.nodes.find(n => n.key === key); @@ -181,7 +197,7 @@ window.galleryState = { const operations = (node.operations || []).map(operation => '
' + esc(operation.label) + '' + '' + (operation.role === 'override' ? 'Implement · engine calls this hook' : 'Application calls') + - '

' + esc(operation.brief) + '

' + + '

' + esc(operation.brief) + '

' + signatureLinks(node, operation) + '
' + (operation.api ? 'Method reference ↗
' : '') + sourceLink(operation) + '
').join(''); @@ -197,6 +213,7 @@ window.galleryState = { : '') + sourceLink(node) + '
') + (operations ? '

Application entry points

' + operations : '') + + (node.kind === 'operation' ? signatureLinks(node, node) : '') + '

Outgoing · ' + outgoing.length + '

' + list(outgoing, true) + '

Incoming · ' + incoming.length + '

' + list(incoming, false) + ''; @@ -215,6 +232,11 @@ window.galleryState = { if (matchMedia('(max-width:800px)').matches) inspect(true); const get = k => current.nodes.find(n => n.key === k); const s = get(e.source), t = get(e.target); + const signatures = [...(s.operations || []), ...(s.kind === 'operation' ? [s] : [])] + .flatMap(operation => signatureKinds(e, operation).map(kind => + '

' + esc(operation.label) + ' ' + + (kind === 'accepts' ? 'accepts ' : 'returns ') + '' + esc(t.label) + '.

')) + .join(''); const examples = (e.examples || []) .map(x => '
' + esc(x.source) + ' → ' + @@ -227,7 +249,7 @@ window.galleryState = { (s.kind === 'role' || t.kind === 'role' ? 'Illustrative application wiring; the following guide establishes the API and ownership contract.' : 'Selected Engine dependency; source evidence is listed below.') + - '

' + examples + '
' + + '

' + signatures + examples + '
' + (e.examples?.length ? '' : e.evidence.map(sourceLink).join('

')) + '
'; $('#back-details').onclick = clear; From f35945a548ac7a52f5e997fda0e48314d977852f Mon Sep 17 00:00:00 2001 From: cppcooper Date: Wed, 21 Oct 2026 11:57:37 -0700 Subject: [PATCH 92/98] Adds richer C++ syntax highlighting to documentation Enriches Pygments' C++ lexer with Tree-sitter roles for types, namespaces, function declarations and calls, parameters, local variables, class fields, and enum values. Tracks scopes within each snippet, preserves lexical tokens and source text, and leaves identifiers with missing context neutral. Reuses the existing generated public API inventory for qualified type, enum and class-member names. Adds no separate source index, compiler database, manually maintained symbol list or browser-side parser. Pins and hash-checks tree-sitter and tree-sitter-cpp in the isolated docs tools, checks their imports during bootstrap, and validates binary wheel metadata coverage for CPython 3.11-3.14 on the supported desktop platforms. Uses distinct, contrasting light/dark colors for semantic roles and the same readable monospace stack for inline names and code blocks. Gives blocks more line spacing and a slightly larger font. Validation: 16 parser/HTML regressions, 44 bootstrap tests and 9 configuration tests passed. All 163 authored C++ fences retained their exact text. Firefox previews passed desktop/narrow checks in both themes; all 11 palette roles exceed 5:1 contrast. Full local documentation generation follows this commit. --- docs/site/generation/bootstrap.py | 5 +- docs/site/generation/conf.py | 2 +- .../generation/extensions/cpp_highlighting.py | 292 ++++++++++++++++++ docs/site/generation/requirements.txt | 53 ++++ docs/site/generation/static/theme.css | 96 ++++-- docs/site/generation/static/typography.css | 9 +- docs/site/generation/tests/test_bootstrap.py | 5 +- .../generation/tests/test_cpp_highlighting.py | 177 +++++++++++ 8 files changed, 610 insertions(+), 29 deletions(-) create mode 100644 docs/site/generation/extensions/cpp_highlighting.py create mode 100644 docs/site/generation/tests/test_cpp_highlighting.py diff --git a/docs/site/generation/bootstrap.py b/docs/site/generation/bootstrap.py index 0e4b20cc..b7d17c7f 100644 --- a/docs/site/generation/bootstrap.py +++ b/docs/site/generation/bootstrap.py @@ -33,6 +33,8 @@ "myst-parser": "5.1.0", "sphinx-rtd-theme": "3.1.0", "sphinxcontrib-mermaid": "2.1.1", + "tree-sitter": "0.26.0", + "tree-sitter-cpp": "0.23.4", } @@ -180,7 +182,8 @@ def environment_identity() -> str: if requirement.specifier and not requirement.specifier.contains(dependency, prereleases=True): raise RuntimeError(f"{name} requires {requirement}, found {dependency}") pending.append((requirement.name, frozenset(getattr(requirement, "extras", ())))) - for name in ("sphinx", "breathe", "exhale", "myst_parser", "sphinx_rtd_theme", "sphinxcontrib.mermaid"): + for name in ("sphinx", "breathe", "exhale", "myst_parser", "sphinx_rtd_theme", "sphinxcontrib.mermaid", + "tree_sitter", "tree_sitter_cpp"): importlib.import_module(name) print(json.dumps({"versions": versions})) except Exception as error: diff --git a/docs/site/generation/conf.py b/docs/site/generation/conf.py index 4cddae95..72be6295 100644 --- a/docs/site/generation/conf.py +++ b/docs/site/generation/conf.py @@ -30,7 +30,7 @@ source_repository = "https://github.com/cppcooper/cheryl-engine" source_repository_root = str(REPO_ROOT) -extensions = ["myst_parser", "sphinxcontrib.mermaid", "breathe", "exhale", "api_rendering", "repository_links", "inline_api", "incremental_api", "engine_diagrams"] +extensions = ["myst_parser", "sphinxcontrib.mermaid", "breathe", "exhale", "api_rendering", "repository_links", "inline_api", "incremental_api", "engine_diagrams", "cpp_highlighting"] # Render ordinary Mermaid fences in the browser using bundled, pinned assets. myst_fence_as_directive = ["mermaid"] mermaid_output_format = "raw" diff --git a/docs/site/generation/extensions/cpp_highlighting.py b/docs/site/generation/extensions/cpp_highlighting.py new file mode 100644 index 00000000..6b59c731 --- /dev/null +++ b/docs/site/generation/extensions/cpp_highlighting.py @@ -0,0 +1,292 @@ +"""Enrich C++ code blocks with syntax roles and the existing public API inventory.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from functools import lru_cache +import re + +from pygments.lexers import CppLexer +from pygments.token import Name +from tree_sitter import Language, Parser +import tree_sitter_cpp + + +TYPE_KINDS = {"class", "union", "type", "concept", "enum"} +SCOPES = { + "translation_unit", "namespace_definition", "class_specifier", "struct_specifier", + "union_specifier", "enum_specifier", "function_definition", "lambda_expression", "compound_statement", + "for_statement", "for_range_loop", "if_statement", "switch_statement", + "while_statement", "catch_clause", +} +CLASS_SCOPES = {"class_specifier", "struct_specifier", "union_specifier"} +WRAPPERS = { + "init_declarator", "pointer_declarator", "reference_declarator", "array_declarator", + "parenthesized_declarator", "attributed_declarator", "variadic_declarator", + "function_declarator", +} + + +def terminal(node): + """Find a declarator's name without descending into arguments or initializers.""" + if node is None: + return None + if node.type in {"qualified_identifier", "template_function", "template_method", "template_type"}: + return terminal(node.child_by_field_name("name")) + if node.type in WRAPPERS: + child = node.child_by_field_name("declarator") + if child is None: + child = next((child for child in node.named_children + if child.type in WRAPPERS or child.type in { + "identifier", "field_identifier", "qualified_identifier", + "structured_binding_declarator", + }), None) + return terminal(child) + if node.type == "destructor_name": + return next((child for child in node.named_children if child.type == "identifier"), None) + if node.type in {"identifier", "field_identifier", "type_identifier", "namespace_identifier"}: + return node + return None + + +def declared_names(node): + if node is None: + return [] + if node.type == "structured_binding_declarator": + return [child for child in node.named_children if child.type == "identifier"] + if node.type in WRAPPERS and node.type != "function_declarator": + child = node.child_by_field_name("declarator") + if child is None: + child = next((child for child in node.named_children + if child.type in WRAPPERS or child.type in { + "identifier", "field_identifier", "structured_binding_declarator", + }), None) + return declared_names(child) + name = terminal(node) + return [name] if name is not None else [] + + +def is_function(node): + while node is not None and node.type in WRAPPERS: + if node.type == "function_declarator": + declarator = node.child_by_field_name("declarator") + return declarator is not None and declarator.type in { + "identifier", "field_identifier", "qualified_identifier", "destructor_name", + } + node = node.child_by_field_name("declarator") + return False + + +@dataclass +class Scope: + kind: str + parent: Scope | None = None + prefix: str = "" + bindings: dict = field(default_factory=dict) + enums: dict = field(default_factory=dict) + + def resolve(self, name, position): + scope = self + while scope is not None: + binding = scope.bindings.get(name) + if binding is not None and binding[1] <= position: + return binding[0] + scope = scope.parent + return None + + +class Roles: + def __init__(self, source, symbols): + self.source = source + self.symbols = symbols + self.annotations = {} + + def text(self, node): + return self.source[node.start_byte:node.end_byte].decode("utf-8") + + def mark(self, node, token, priority=10): + if node is not None and not node.is_missing: + key = (node.start_byte, node.end_byte) + if key not in self.annotations or self.annotations[key][0] < priority: + self.annotations[key] = (priority, token) + + def bind(self, scope, node, token, *, position=None): + self.mark(node, token, 50) + scope.bindings[self.text(node)] = (token, node.start_byte if position is None else position) + + def qualified(self, node, scope): + name = re.sub(r"\s+", "", self.text(node)).removeprefix("::") + candidates = [name] + current = scope + while current is not None: + if current.prefix: + candidates.append(current.prefix + "::" + name) + current = current.parent + for key in candidates: + current = scope + while current is not None: + if key in current.enums: + return current.enums[key] + current = current.parent + if key in self.symbols: + return self.symbols[key] + return None + + def visit(self, node, scope=None): + if node.type in SCOPES: + prefix = scope.prefix if scope is not None else "" + name = node.child_by_field_name("name") + if name is not None and scope is not None and node.type in CLASS_SCOPES | {"enum_specifier"}: + self.bind(scope, name, Name.Class) + if name is not None and node.type in CLASS_SCOPES | {"enum_specifier", "namespace_definition"}: + prefix = "::".join(part for part in (prefix, self.text(name)) if part) + scope = Scope(node.type, scope, prefix) + if node.type in CLASS_SCOPES: + # All fields are visible throughout a class, including earlier methods. + body = node.child_by_field_name("body") + for member in body.named_children if body is not None else (): + if member.type == "field_declaration": + for declarator in member.children_by_field_name("declarator"): + if not is_function(declarator): + for name in declared_names(declarator): + self.bind(scope, name, Name.Variable.Instance, position=0) + + kind = node.type + if kind in {"type_identifier", "namespace_identifier"}: + self.mark(node, Name.Class if kind == "type_identifier" else Name.Namespace) + elif kind == "field_identifier": + self.mark(node, Name.Variable.Instance) + elif kind == "identifier": + token = scope.resolve(self.text(node), node.start_byte) + if token is not None: + self.mark(node, token) + elif kind == "qualified_identifier": + token = self.qualified(node, scope) + if token is not None: + self.mark(terminal(node), token, 45) + elif kind == "function_declarator": + declarator = node.child_by_field_name("declarator") + if is_function(node): + self.mark(terminal(declarator), Name.Function, 50) + # Prototype parameters must not leak into the enclosing snippet's scope. + ancestor = node.parent + while ancestor is not None and ancestor.type in WRAPPERS: + ancestor = ancestor.parent + if ancestor is None or ancestor.type not in {"function_definition", "lambda_expression"}: + scope = Scope(kind, scope, scope.prefix) + elif kind == "call_expression": + function = node.child_by_field_name("function") + if function is not None: + if function.type == "field_expression": + function = function.child_by_field_name("field") + name = terminal(function) + known = scope.resolve(self.text(name), name.start_byte) if name is not None else None + self.mark(name, Name.Class if known == Name.Class else Name.Function.Call, 40) + elif kind in {"parameter_declaration", "optional_parameter_declaration"}: + for name in declared_names(node.child_by_field_name("declarator")): + self.bind(scope, name, Name.Variable.Parameter) + elif kind in {"declaration", "field_declaration", "for_range_loop"}: + token = Name.Variable.Instance if kind == "field_declaration" else Name.Variable + for declarator in node.children_by_field_name("declarator"): + for name in declared_names(declarator): + self.bind(scope, name, Name.Function if is_function(declarator) else token) + elif kind == "lambda_capture_initializer": + for name in declared_names(node.child_by_field_name("left")): + self.bind(scope, name, Name.Variable) + elif kind == "enum_specifier": + name = node.child_by_field_name("name") + body = node.child_by_field_name("body") + scoped = any(child.type in {"class", "struct"} for child in node.children) + for value in body.named_children if body is not None else (): + enumerator = value.child_by_field_name("name") + if enumerator is not None: + self.bind(scope, enumerator, Name.Constant) + if not scoped and scope.parent is not None: + self.bind(scope.parent, enumerator, Name.Constant) + if name is not None: + qualified = "::".join(part for part in ( + scope.prefix, self.text(enumerator) + ) if part) + owner = scope.parent + while owner.parent is not None and owner.kind in CLASS_SCOPES | {"namespace_definition"}: + owner = owner.parent + owner.enums[qualified] = Name.Constant + + for child in node.named_children: + self.visit(child, scope) + + +@lru_cache(maxsize=1) +def language(): + return Language(tree_sitter_cpp.language()) + + +class DocumentationCppLexer(CppLexer): + """Preserve Pygments' lexical tokens and add roles established by C++ syntax.""" + + public_symbols = {} + + def get_tokens_unprocessed(self, text, stack=("root",)): + source = text.encode("utf-8") + roles = Roles(source, self.public_symbols) + roles.visit(Parser(language()).parse(source).root_node) + # Tree-sitter uses UTF-8 byte offsets; Pygments uses Python character offsets. + boundaries = sorted({position for pair in roles.annotations for position in pair}) + offsets = {} + previous = characters = 0 + for position in boundaries: + characters += len(source[previous:position].decode("utf-8")) + offsets[position] = characters + previous = position + spans = [(offsets[start], offsets[end], token) + for (start, end), (_, token) in sorted(roles.annotations.items())] + cursor = 0 + for start, token, value in super().get_tokens_unprocessed(text, stack): + end = start + len(value) + if token not in Name: + yield start, token, value + continue + # Pygments combines qualified function names into one token. + base = Name if "::" in value else token + position = start + while cursor < len(spans) and spans[cursor][1] <= start: + cursor += 1 + index = cursor + while index < len(spans) and spans[index][0] < end: + left, right, replacement = spans[index] + if position < left: + yield position, base, text[position:left] + stop = min(end, right) + yield max(position, left), replacement, text[max(position, left):stop] + position = stop + if right > end: + break + index += 1 + if position < end: + yield position, base, text[position:end] + + +def prepare(app, environment): + # inline_api already builds this inventory from Sphinx's generated declarations. + symbols = {} + classes = {name for name, targets in app._cheryl_inline_api.exact.items() + if {target.kind for target in targets} <= {"class", "union"}} + for name, targets in app._cheryl_inline_api.exact.items(): + kinds = {target.kind for target in targets} + if len(kinds) == 1: + kind = next(iter(kinds)) + if kind in TYPE_KINDS: + symbols[name] = Name.Class + elif kind == "enumerator": + symbols[name] = Name.Constant + elif kind == "member" and name.rpartition("::")[0] in classes: + symbols[name] = Name.Variable.Instance + DocumentationCppLexer.public_symbols = symbols + + +def setup(app): + app.setup_extension("inline_api") + for name in ("cpp", "c++"): + app.add_lexer(name, DocumentationCppLexer) + app.connect("env-updated", prepare, priority=600) + return {"version": "1", "parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/site/generation/requirements.txt b/docs/site/generation/requirements.txt index 33a32cb0..e3024238 100644 --- a/docs/site/generation/requirements.txt +++ b/docs/site/generation/requirements.txt @@ -689,6 +689,59 @@ sphinxcontrib-qthelp==2.0.0 \ sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 +# C++ parser wheels: official PyPI release JSON, retrieved 2026-10-21. + +tree-sitter==0.26.0 \ + --hash=sha256:00289bfe7978f3e0dc0ce69813a20fa9f44ea4c100b3ec62043e5eb74ccfc3a2 \ + --hash=sha256:0f8793fd18ad7eec276ed4b51c097b4bf2002b357259b66b0d75db1f3f41c754 \ + --hash=sha256:10f0d4eb94aa7242dcb7f554bcd24dd7ba1c114f00d58759ba08c7a46c8ec51a \ + --hash=sha256:17a1c5cfd3a05d5c7c86bf4282b6ef8092c91dc0a98390499669c3fedb7d1814 \ + --hash=sha256:1d6fe0e8fb4df77b5ee816228e2c4475a63d8cc1d4d3a7ffd7097b2b87fc3e95 \ + --hash=sha256:253df7ab82cc0a9d311cd65f06e9f99fb3eac55996ae9fc94da22f123a861b90 \ + --hash=sha256:26c996c1edfee86e977bb3f5462e74fcec0d0b0db1e85a3c475875763caa03be \ + --hash=sha256:2f941cea06128c1f74f8937a8e2a90c7db49cf4be6647cd9e07d92a306d91517 \ + --hash=sha256:30a88be89ff1f2755297f81e8080d88b795dd98720c3f9fa2acf93873182cc95 \ + --hash=sha256:335294ce0504fcefde5245dff596778ffaf820205b98ae0b549c72e48855f1d8 \ + --hash=sha256:3f3c44339dd34fe8eb2b8d5aa7610660499a795f70376b130bbee7a437337280 \ + --hash=sha256:514a9bf8993e5210e7970736aaf6020d1759b670e195ef17b1c48f586aa30736 \ + --hash=sha256:526a165a2cb1d1f79e247d400f0e0acd8d49a817d6f312d543513af200b1f886 \ + --hash=sha256:5a3c93a352b7e6f70f73e121bbfa2d0117ba7478bd51114ed35c91b0b78814fa \ + --hash=sha256:5a6b333b0282d8bb0af741f9b018bd2523d4eecb2686bf6717066a625fecfaa4 \ + --hash=sha256:5fc2f41bf246ff2f70a9cc3690be35ec7580a4923151873d898c8bcb1a4503d3 \ + --hash=sha256:6189c6c340c7384357711e3d92645e96bfb79f7a502f86de1ebdb23eb43f7dab \ + --hash=sha256:6cb2bd20efb2544c19ac54486ab7cb8ec7b36f913bbe1ce95df84acb96743d9c \ + --hash=sha256:7075ef857ef86f327dbb72d1e2574dda78db5754b3a1fca6506acd7fe5d561a7 \ + --hash=sha256:763627db05db34f12333081bd7422cc1c675893d373cc870b3e9249e200700e4 \ + --hash=sha256:7bcbadfa614326debef581957d5c780a9d7f66065c13deea61aa21d1dd36263f \ + --hash=sha256:823251c4b6725a7c03ed497a339135ede7ae4bdde75bb8be7ef5e305aeb4ff52 \ + --hash=sha256:8ff2e0750b7daa722302838356d7b65e303829b7eb73c915df127ddba115e1d1 \ + --hash=sha256:918d89529786873f0982a0f59c2a303cd065fbfd1b903d71a8e4e1584f67b42e \ + --hash=sha256:93e220cab7e6a823efeb2046c49171427de92ef71c7c681c01820d14d8d3721f \ + --hash=sha256:94550e13b6ae576969da40246f4c4abb206380b5375ad43f26dd9151d55438e3 \ + --hash=sha256:a4033fecc8f606c7f2e8b8014d0057b74668a7f0152763606f7bc25c5f9ec64c \ + --hash=sha256:b31a8195d2f224224c530ac814632d98c1dcc123d227442c07c736e86b70d564 \ + --hash=sha256:b8ea92a255c91671a7ec4625aba3ab7bb5220c423630ffbf83c45d7312abe084 \ + --hash=sha256:bc6cb01d5ee75c85424aa1f1c72a82d8f07fd52539a0f3c4a6ed3e8721079b84 \ + --hash=sha256:c56581ad256c4195a21bfe449fed5d44a02fe83a4a7d6e70e6ec302c881191c7 \ + --hash=sha256:ca89e361a276dbc934b28a43dd881199e25d34ff5493ee0ce45f3c52a6124a37 \ + --hash=sha256:dea4b4e27d49e9ec5b785d4f994da000e6726882fcc6ad05ec98478500c71aef \ + --hash=sha256:e9e46b664887d8c1014f1fb33e09454bbdd9ec1fe29b7fd02dde7b46bc1bb81a \ + --hash=sha256:ed0889dbed843ce45ede9f5169c0b2dea2222f12685844a03fadb81f12705867 \ + --hash=sha256:f289be0225ba2ace8e87d6c9639b2bc9ff2b5271afb7c5d39282a4a00e248682 \ + --hash=sha256:f665510f0fcf4636fb9696f1f7853bed7a3bd764b7bb0cb8494e619c14ed5a0c \ + --hash=sha256:f9997ba61368c48ed54e715676afadf703947a1542464e39d047764fb3624b01 \ + --hash=sha256:ff527388df14cb5009f9274faf78cc69a7393ae6acf3b04784b8acca249519c5 \ + --hash=sha256:ff80d4833d330a73184a3ac5132abe93c575d2dea31975c6f15c0d21fef238aa + +tree-sitter-cpp==0.23.4 \ + --hash=sha256:247d127f0eb6574b0f6b30c0151e0bd0774e2e7acf9c558bdf9fbb8adc2e80c0 \ + --hash=sha256:68606a45bea92669d155399e1239f771a7767d8683cd8f8e30e7d813107030ca \ + --hash=sha256:712f84f18be94cbe2a148fa4fdf40fcf4a8c25a8f7670efb9f8a47ddec2fc281 \ + --hash=sha256:773d2cafc08bbc0f998687fa33f42f378c1a371cdb582870c4d13abb06092706 \ + --hash=sha256:aacb1759f0efd9dbc25bd8ee88184a340483018869f75412d9c3bc32c039a520 \ + --hash=sha256:bc3c404d9f0cbd87951213a85440afbf4c31e718f8d907fa9ee12bea4b8d276f \ + --hash=sha256:ccc43ddf1279d5d5a4ef190373f4cb16522801bec4492bcd4754edf2aeba2b7b + typing-extensions==4.16.0 \ --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 diff --git a/docs/site/generation/static/theme.css b/docs/site/generation/static/theme.css index f4e48239..8d0b4d8f 100644 --- a/docs/site/generation/static/theme.css +++ b/docs/site/generation/static/theme.css @@ -1,3 +1,64 @@ +:root { + --cheryl-syntax-keyword: #b4233c; + --cheryl-syntax-type: #805500; + --cheryl-syntax-definition: #2a6b38; + --cheryl-syntax-call: #006c72; + --cheryl-syntax-parameter: #5b6572; + --cheryl-syntax-local: #1856a1; + --cheryl-syntax-member: #a33367; + --cheryl-syntax-constant: #985100; + --cheryl-syntax-string: #7e4e16; + --cheryl-syntax-comment: #5d6b7b; + --cheryl-syntax-annotation: #7746a2; +} + +/* Token roles stay consistent between the generated light and dark palettes. */ +.highlight :is(.c, .ch, .cm, .cpf, .c1, .cs, .go, .w) { + color: var(--cheryl-syntax-comment); +} + +.highlight :is(.k, .kc, .kd, .kn, .kp, .kr, .kt, .nt, .ow) { + color: var(--cheryl-syntax-keyword); + font-weight: 500; +} + +.highlight :is(.cp, .nd) { + color: var(--cheryl-syntax-annotation); +} + +.highlight :is(.m, .mb, .mf, .mh, .mi, .mo, .il, .no) { + color: var(--cheryl-syntax-constant); +} + +.highlight :is(.s, .sa, .sb, .sc, .dl, .sd, .s1, .s2, .sh, .sx, .ss, .se, .si, .sr) { + color: var(--cheryl-syntax-string); +} + +.highlight :is(.nc, .nn) { + color: var(--cheryl-syntax-type); + font-weight: 500; +} + +.highlight :is(.nf, .fm) { + color: var(--cheryl-syntax-definition); +} + +.highlight :is(.nb, .bp, .nf-Call) { + color: var(--cheryl-syntax-call); +} + +.highlight :is(.nv, .vg, .vm) { + color: var(--cheryl-syntax-local); +} + +.highlight :is(.vi, .vc) { + color: var(--cheryl-syntax-member); +} + +.highlight .nv-Parameter { + color: var(--cheryl-syntax-parameter); +} + /* Give inline names a quiet surface and readable light-theme contrast. */ .rst-content :is(code.literal, tt.literal) { border-color: #d8dee6; @@ -70,6 +131,17 @@ --cheryl-visited: #d0b2ff; --cheryl-focus: #ffda82; --cheryl-mark: #594a22; + --cheryl-syntax-keyword: #ff8899; + --cheryl-syntax-type: #eed28c; + --cheryl-syntax-definition: #a6d98a; + --cheryl-syntax-call: #76d7ca; + --cheryl-syntax-parameter: #b3bfcd; + --cheryl-syntax-local: #91baff; + --cheryl-syntax-member: #f0a1c2; + --cheryl-syntax-constant: #ffb875; + --cheryl-syntax-string: #cad99a; + --cheryl-syntax-comment: #a5afbf; + --cheryl-syntax-annotation: #c8acff; scrollbar-color: var(--cheryl-border) var(--cheryl-background); } @@ -378,31 +450,11 @@ color: var(--cheryl-muted); } - html[data-theme="dark"] .highlight :is(.c, .ch, .cm, .cpf, .c1, .cs, .go, .w) { - color: #a8b8cb; - } - - html[data-theme="dark"] .highlight :is(.cp, .k, .kc, .kd, .kn, .kp, .kr, .kt, .nt, .ow) { - color: #c3a6ff; - } - - html[data-theme="dark"] .highlight :is(.m, .mb, .mf, .mh, .mi, .mo, .il, .no) { - color: #ffca91; - } - - html[data-theme="dark"] .highlight :is(.s, .sa, .sb, .sc, .dl, .sd, .s1, .s2, .sh, .sx, .ss) { - color: #a9dca1; - } - - html[data-theme="dark"] .highlight :is(.se, .si, .sr, .na, .nd, .nl) { - color: #f3d889; - } - - html[data-theme="dark"] .highlight :is(.nb, .bp, .nc, .nf, .fm, .nn, .gh, .gp, .gu, .gt) { + html[data-theme="dark"] .highlight :is(.na, .nl, .gh, .gp, .gu, .gt) { color: #8ecaff; } - html[data-theme="dark"] .highlight :is(.ni, .nv, .vc, .vg, .vi, .vm, .o) { + html[data-theme="dark"] .highlight :is(.ni, .o) { color: var(--cheryl-text); } diff --git a/docs/site/generation/static/typography.css b/docs/site/generation/static/typography.css index 215aa602..00812d1d 100644 --- a/docs/site/generation/static/typography.css +++ b/docs/site/generation/static/typography.css @@ -1,6 +1,8 @@ /* Enlarge the RTD theme's type by 20%, including its fixed-size text. */ html { font-size: 120%; + --cheryl-monospace: ui-monospace, "Cascadia Code", "Source Code Pro", "DejaVu Sans Mono", + "Noto Sans Mono", "SFMono-Regular", Menlo, Consolas, monospace; } /* Scale prose with the root size. */ @@ -11,8 +13,7 @@ p { /* Keep inline names close to prose size without the Courier-style fallback. */ .rst-content :is(code.literal, tt.literal) { - font-family: ui-monospace, "Cascadia Code", "Source Code Pro", "DejaVu Sans Mono", - "Noto Sans Mono", "SFMono-Regular", Menlo, Consolas, monospace; + font-family: var(--cheryl-monospace); font-size: 0.9em; font-weight: 500; padding: 0.05em 0.3em; @@ -22,7 +23,9 @@ p { .rst-content .linenodiv pre, .rst-content div[class^="highlight"] pre, .rst-content pre.literal-block { - font-size: 0.75rem; + font-family: var(--cheryl-monospace); + font-size: 0.8125rem; + line-height: 1.55; } .wy-menu-vertical a { diff --git a/docs/site/generation/tests/test_bootstrap.py b/docs/site/generation/tests/test_bootstrap.py index 642d1858..618d4092 100644 --- a/docs/site/generation/tests/test_bootstrap.py +++ b/docs/site/generation/tests/test_bootstrap.py @@ -156,7 +156,8 @@ def test_python_change(self): class ProbeTests(TemporaryCase): """Use real dist-info discovery; stub only the external packaging parser.""" - imports = ("sphinx", "breathe", "exhale", "myst_parser", "sphinx_rtd_theme", "sphinxcontrib.mermaid") + imports = ("sphinx", "breathe", "exhale", "myst_parser", "sphinx_rtd_theme", "sphinxcontrib.mermaid", + "tree_sitter", "tree_sitter_cpp") def setUp(self): super().setUp() @@ -303,7 +304,7 @@ def test_dependency_alias(self): def test_missing_import(self): self.distribution("Fixture-Doc", "1.0.0") - for name in ("breathe", "sphinxcontrib.mermaid"): + for name in ("breathe", "sphinxcontrib.mermaid", "tree_sitter", "tree_sitter_cpp"): with self.subTest(module=name): module = self.site / f"{name}.py" module.unlink() diff --git a/docs/site/generation/tests/test_cpp_highlighting.py b/docs/site/generation/tests/test_cpp_highlighting.py new file mode 100644 index 00000000..831c698e --- /dev/null +++ b/docs/site/generation/tests/test_cpp_highlighting.py @@ -0,0 +1,177 @@ +"""C++ highlighting regressions using the installed documentation parser packages.""" + +from pathlib import Path +import sys +import types +import unittest +from unittest.mock import Mock, patch + +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import CppLexer +from pygments.token import Comment, Keyword, Name, String + + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "extensions")) +import cpp_highlighting as highlighting + + +EXAMPLE = """void SpaceInvaders::prepare_render_frame(CE::Rendering::FrameWriter& frame) const { + auto pass = frame.begin_pass(camera.projection_matrix(), camera.view_matrix()); + const CE::Rendering::Submission::Context2D context{pass.semantics(), pass.parameters(), pass.constraints()}; +""" + + +class HighlightingTests(unittest.TestCase): + def setUp(self): + self.enterContext(patch.object(highlighting.DocumentationCppLexer, "public_symbols", {})) + self.lexer = highlighting.DocumentationCppLexer() + + def tokens(self, source): + tokens = list(self.lexer.get_tokens_unprocessed(source)) + self.assertEqual("".join(value for _, _, value in tokens), source) + for start, _, value in tokens: + self.assertEqual(source[start:start + len(value)], value) + return tokens + + def roles(self, source, word): + return [token for _, token, value in self.tokens(source) if value == word] + + def test_render_example(self): + expected = { + "SpaceInvaders": Name.Namespace, + "prepare_render_frame": Name.Function, + "FrameWriter": Name.Class, + "Context2D": Name.Class, + "frame": Name.Variable.Parameter, + "pass": Name.Variable, + "context": Name.Variable, + "begin_pass": Name.Function.Call, + "projection_matrix": Name.Function.Call, + "camera": Name, + "const": Keyword, + } + for word, token in expected.items(): + with self.subTest(word=word): + roles = self.roles(EXAMPLE, word) + self.assertTrue(roles) + self.assertEqual(set(roles), {token}) + html = highlight(EXAMPLE, self.lexer, HtmlFormatter()) + self.assertRegex(html, r'class="[^"]*\bnf-Call\b[^"]*">begin_pass') + self.assertRegex(html, r'class="[^"]*\bnv-Parameter\b[^"]*">frame') + self.assertIn('class="nf">prepare_render_frame', html) + + def test_shadowed_parameter(self): + source = "void draw(int value) { use(value); { int value = 2; use(value); } use(value); }" + self.assertEqual(self.roles(source, "value"), [ + Name.Variable.Parameter, Name.Variable.Parameter, + Name.Variable, Name.Variable, Name.Variable.Parameter, + ]) + + def test_prototype_scope(self): + source = "void draw(int value); use(value); void other(int value) { use(value); }" + self.assertEqual(self.roles(source, "value"), [ + Name.Variable.Parameter, Name, Name.Variable.Parameter, Name.Variable.Parameter, + ]) + + def test_member_call_and_local(self): + source = "auto begin_pass = 1; use(begin_pass); frame.begin_pass();" + self.assertEqual(self.roles(source, "begin_pass"), [ + Name.Variable, Name.Variable, Name.Function.Call, + ]) + + def test_class_fields(self): + source = "class Game { void draw(int camera) { use(camera); use(this->camera); } Camera camera; };" + self.assertEqual(self.roles(source, "camera"), [ + Name.Variable.Parameter, Name.Variable.Parameter, + Name.Variable.Instance, Name.Variable.Instance, + ]) + source = "class Game { void draw() { use(camera); } Camera camera; };" + self.assertEqual(self.roles(source, "camera"), [Name.Variable.Instance] * 2) + + def test_local_constructor(self): + source = "class Game {}; auto game = Game();" + self.assertEqual(self.roles(source, "Game"), [Name.Class, Name.Class]) + + def test_scoped_enum(self): + source = "enum class Mode { active, next = active }; auto mode = Mode::active; use(active);" + self.assertEqual(self.roles(source, "active"), [ + Name.Constant, Name.Constant, Name.Constant, Name, + ]) + + def test_unscoped_enum(self): + source = "namespace CE { enum Mode { active }; auto mode = active; auto other = Mode::active; }" + self.assertEqual(self.roles(source, "active"), [Name.Constant] * 3) + + def test_enum_visibility(self): + source = "void one() { enum class Mode { active }; use(Mode::active); } void two() { use(Mode::active); }" + self.assertEqual(self.roles(source, "active"), [Name.Constant, Name.Constant, Name]) + source = "namespace CE { enum class Mode { active }; } namespace CE { auto mode = Mode::active; }" + self.assertEqual(self.roles(source, "active"), [Name.Constant, Name.Constant]) + + def test_generated_symbols(self): + target = lambda kind: types.SimpleNamespace(kind=kind) + app = types.SimpleNamespace(_cheryl_inline_api=types.SimpleNamespace(exact={ + "CE::Mode::active": [target("enumerator")], + "CE::Frame": [target("class")], + "CE::Frame::time": [target("member")], + "CE::global": [target("member")], + "CE::unknown": [target("class"), target("enumerator")], + })) + highlighting.prepare(app, None) + source = "auto mode = CE::Mode::active; auto frame = CE::Frame(); use(active);" + self.assertEqual(self.roles(source, "active"), [Name.Constant, Name]) + self.assertEqual(self.roles(source, "Frame"), [Name.Class]) + self.assertEqual(self.roles("auto time = CE::Frame::time;", "time"), [ + Name.Variable, Name.Variable.Instance, + ]) + self.assertEqual(self.roles("use(CE::global);", "global"), [Name]) + self.assertNotIn("CE::unknown", self.lexer.public_symbols) + source = "namespace CE { auto mode = Mode::active; }" + self.assertEqual(self.roles(source, "active"), [Name.Constant]) + app._cheryl_inline_api.exact = {} + highlighting.prepare(app, None) + self.assertEqual(self.roles(source, "active"), [Name]) + + def test_range_scope(self): + source = "for (auto [key, value] : entries) { use(key, value); } use(key);" + self.assertEqual(self.roles(source, "key"), [Name.Variable, Name.Variable, Name]) + self.assertEqual(self.roles(source, "value"), [Name.Variable, Name.Variable]) + + def test_lambda_scope(self): + source = "auto draw = [count = 2](int value) { use(count, value); }; use(count, value);" + self.assertEqual(self.roles(source, "count"), [Name.Variable, Name.Variable, Name]) + self.assertEqual(self.roles(source, "value"), [ + Name.Variable.Parameter, Name.Variable.Parameter, Name, + ]) + + def test_function_pointer(self): + source = "void (*callback)(int argument); callback(2); int *factory();" + self.assertEqual(self.roles(source, "callback"), [Name.Variable, Name.Function.Call]) + self.assertEqual(self.roles(source, "factory"), [Name.Function]) + + def test_unicode_and_literals(self): + source = '// caméra → draw()\nconst char* text = R"(camera.draw())";\nauto pass = draw("é");' + original = [(token, value) for _, token, value in CppLexer().get_tokens_unprocessed(source) + if token in Comment or token in String] + enriched = [(token, value) for _, token, value in self.tokens(source) + if token in Comment or token in String] + self.assertEqual(enriched, original) + self.assertEqual(self.roles(source, "pass"), [Name.Variable]) + + def test_partial_code(self): + for source in ("", "frame.begin_pass(", "auto pass = frame.begin_pass();", EXAMPLE, "...\n}"): + with self.subTest(source=source): + self.tokens(source) + + def test_registration(self): + app = Mock() + metadata = highlighting.setup(app) + app.setup_extension.assert_called_once_with("inline_api") + self.assertEqual([call.args[0] for call in app.add_lexer.call_args_list], ["cpp", "c++"]) + app.connect.assert_called_once_with("env-updated", highlighting.prepare, priority=600) + self.assertTrue(metadata["parallel_write_safe"]) + + +if __name__ == "__main__": + unittest.main() From 9265a81544f760ae19212900dd468662c22d1c95 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Wed, 21 Oct 2026 12:22:41 -0700 Subject: [PATCH 93/98] Adds argument and result types to application diagrams Shows qualified public types across all 17 application views, including ownership, borrowed collections and deferred results where standard wrappers are abridged. Keeps free-function labels specific to their selected views and preserves dependency arrows, symbol identity and API links. Wraps long signatures and documents the display conventions. Checks that wrapped labels retain template and result types, and that free-function labels do not leak into other views. Verification: 16 focused diagram tooling tests, JavaScript syntax and diff checks pass. Full cached documentation generation and rendered browser verification follow this commit. --- docs/site/development/documentation.md | 29 +- docs/site/engine-systems/diagram-explorer.md | 7 + .../diagrams/assets-application.mmd | 14 +- .../diagrams/audio-application.mmd | 12 +- .../site/engine-systems/diagrams/catalog.json | 393 ++++++++++-------- .../diagrams/diagnostics-application.mmd | 10 +- .../diagrams/dispatch-application.mmd | 14 +- .../diagrams/display-application.mmd | 10 +- .../diagrams/events-application.mmd | 8 +- .../diagrams/gui-application.mmd | 12 +- .../diagrams/input-application.mmd | 14 +- .../diagrams/logging-application.mmd | 8 +- .../diagrams/memory-application.mmd | 4 +- .../diagrams/rendering-application.mmd | 8 +- .../diagrams/resources-application.mmd | 8 +- .../diagrams/runtime-application.mmd | 12 +- .../diagrams/state-application.mmd | 8 +- .../diagrams/text-application.mmd | 16 +- .../diagrams/utilities-application.mmd | 14 +- .../diagrams/workers-application.mmd | 10 +- docs/site/generation/diagram_catalog.py | 21 +- .../static/engine-diagrams/explorer.mjs | 2 +- .../generation/tests/test_engine_diagrams.py | 47 +++ 23 files changed, 401 insertions(+), 280 deletions(-) diff --git a/docs/site/development/documentation.md b/docs/site/development/documentation.md index 3eb81e54..7376c2cb 100644 --- a/docs/site/development/documentation.md +++ b/docs/site/development/documentation.md @@ -188,17 +188,36 @@ entry points per type, with value types left as values. Each entry records the qualified declaration name, short display label, source file/line, purpose and `role` (`call` or `override`). Use the declaring base type for inherited methods. Include the same labels beneath the owning type in the Mermaid source; label -checks reject stale metadata. Mark override groups **Implement hooks** to -distinguish engine callbacks from application calls. The explorer adds method -search, source links and unambiguous API links without changing the type's identity -or adding operation details to the internal and focused views. +checks reject stale metadata. Wrapping markup may change whitespace, but must +preserve type spelling, including template arguments. Mark override groups +**Implement hooks** to distinguish engine callbacks from application calls. The +explorer adds method search, source links and unambiguous API links without +changing the type's identity or adding operation details to the internal and +focused views. + +Write labels as `method(arguments): QualifiedResultType`. Include relevant +Engine argument and result types, including public aliases and types without +their own box. Keep argument names and the declaration's direct `const`, pointer +and reference qualifiers. Omit defaults, the leading `CE::`, and primitive or +`std::` type spellings. Retain names for scalar and library-only arguments; +include template parameter types where they clarify generic arguments. A +missing result suffix does not imply `void`: scalar and +library-only results are abridged too. Use `[]` for collections, with short +markers such as `[shared]`, `[unique]`, `[future]` or `[borrowed]` where hiding a +library wrapper would otherwise obscure ownership, asynchronous completion or +borrowing. The API reference remains the complete signature. + +For a named free-function box, use the view's `operation_labels` mapping from +its existing node key to the typed label. Keep the global symbol identity and +other views' labels unchanged. Match this label in the Mermaid source; its +validation also ignores wrapping whitespace while preserving template types. Connect selected methods to their public Engine argument and result types when those types are shown in the application view. Record signature evidence with the exact selected declaration name in `member` and `kind: accepts` or `kind: returns`. The explorer uses these records for each method's **Accepts** and **Returns** links and the arrow's method explanation. Use explicit parameter -types in a method label when a short name could refer to multiple shown types. +types in method labels even when that type has no box in the selected view. Add another type box only when it clarifies an essential application boundary; the views remain selective rather than duplicating the complete API inventory. diff --git a/docs/site/engine-systems/diagram-explorer.md b/docs/site/engine-systems/diagram-explorer.md index 2a68be01..27460e16 100644 --- a/docs/site/engine-systems/diagram-explorer.md +++ b/docs/site/engine-systems/diagram-explorer.md @@ -11,6 +11,13 @@ sequence. **Implement hooks** marks methods your game overrides for the engine to call. Select a box for each method's purpose, source and available API link; search also finds method names. +Signatures use `method(arguments): ResultType`, with qualified Engine types and +public aliases for arguments and results. They are abridged: a missing result +suffix can mean a scalar or standard-library result and does not imply `void`. +`[]` marks a collection. Short notes such as `[shared]`, `[unique]`, `[future]` +and `[borrowed]` retain ownership, asynchronous-result and borrowing information +when their library wrappers are omitted. The API reference has the full types. + Application arrows include the argument and result types used by selected methods. In the inspector, **Accepts** and **Returns** connect a method to those types in the current view. Select an arrow to see the supporting method diff --git a/docs/site/engine-systems/diagrams/assets-application.mmd b/docs/site/engine-systems/diagrams/assets-application.mmd index 1aa51892..23cb133e 100644 --- a/docs/site/engine-systems/diagrams/assets-application.mmd +++ b/docs/site/engine-systems/diagrams/assets-application.mmd @@ -15,19 +15,19 @@ flowchart LR accTitle: Assets · application loading and playback accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
loading work"]):::app - k1["Assets::Loading::
Loader


load_assets()
prepare()
upload()"]:::focal + k1["Assets::Loading::
Loader


load_assets(Rendering::Resources::Provider& provider)

prepare(): Assets::Loading::Prepared

upload(
Assets::Loading::Prepared prepared,
Rendering::Resources::Provider& provider,
replace_shaders)"]:::focal k2["Assets::Loading::
Prepared"]:::focal k3(["Application
init_platform()"]):::app k4["Rendering::Resources::
Provider"]:::neighbor k5(["Application-owned game objects"]):::app - k6["Assets::Sprites::
Manager


get_asset()"]:::focal - k7["Assets::Tilesets::
Manager


get_asset()"]:::focal - k8["Assets::Sprites::
Asset


animation()"]:::focal - k9["Assets::Tilesets::
Asset


tile()
animation()
cell_at()"]:::focal + k6["Assets::Sprites::
Manager


get_asset(name_space, name):
Assets::Sprites::Manager::Handle [shared]"]:::focal + k7["Assets::Tilesets::
Manager


get_asset(key):
Assets::Tilesets::Manager::Handle [shared]"]:::focal + k8["Assets::Sprites::
Asset


animation(animation, facing): Assets::Sprites::Animation"]:::focal + k9["Assets::Tilesets::
Asset


tile(cell): Assets::Tilesets::Tile

animation(name): Assets::Tilesets::Animation

cell_at(Assets::Grids::CellIndex target, elapsed):
Assets::Grids::CellIndex"]:::focal k10(["Application
update()"]):::app - k11["Assets::Sprites::
Animation


advance()
cell()"]:::focal + k11["Assets::Sprites::
Animation


advance(elapsed)

cell(): Assets::Grids::CellIndex"]:::focal k12(["Application
prepare_render_frame()"]):::app - k13("Rendering::Submission::
resolve_sprite()"):::neighbor + k13("Rendering::Submission::resolve_sprite(
const Assets::Sprites::Asset& sprite,
Assets::Grids::CellIndex cell,
const Rendering::DrawStyle2D& style,
const Rendering::Submission::Context2D& context):
Rendering::DrawPacket2D"):::neighbor k14["Rendering::
DrawPacket2D"]:::neighbor k0 --> k1 k1 --> k2 diff --git a/docs/site/engine-systems/diagrams/audio-application.mmd b/docs/site/engine-systems/diagrams/audio-application.mmd index 50dbf281..f1992141 100644 --- a/docs/site/engine-systems/diagrams/audio-application.mmd +++ b/docs/site/engine-systems/diagrams/audio-application.mmd @@ -15,17 +15,17 @@ flowchart TB accTitle: Audio · application-owned playback and outcomes accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app - k1["Audio::Miniaudio::
System


open_device()
publish_events_to()
pump_with()
publish_notifications()"]:::focal + k1["Audio::Miniaudio::
System


open_device(
Audio::Format output,
Audio::PlaybackNotificationOptions notifications):
Audio::Miniaudio::System [unique]

publish_events_to(Events::EventBus::Publisher publisher)

pump_with(const Runtime::RuntimeServices& services):
Runtime::ServiceAttachment

publish_notifications()"]:::focal k2(["Application
loading work"]):::app - k3("Audio::
Miniaudio::decode_file()"):::focal - k4["Audio::
Clip


from_samples()"]:::focal + k3("Audio::Miniaudio::decode_file(
file,
Audio::Miniaudio::DecodeOptions options):
Audio::Clip"):::focal + k4["Audio::
Clip


from_samples(Audio::Format format, samples): Audio::Clip"]:::focal k5(["Application
sound controller"]):::app - k6["Audio::
System


play()
stream()
maintain()
close()"]:::focal + k6["Audio::
System


play(
const Audio::Clip& clip,
Audio::PlaybackOptions options):
Audio::Voice [shared]

stream(file, Audio::PlaybackOptions options):
Audio::Voice [shared]

maintain()

close()"]:::focal k7["Audio::
PlaybackOptions"]:::focal - k8["Audio::
Voice


snapshot()
pause()
resume()
stop()"]:::focal + k8["Audio::
Voice


snapshot(): Audio::VoiceSnapshot

pause()

resume()

stop()"]:::focal k9["Audio::
VoiceSnapshot"]:::focal k10(["Application
listener setup"]):::app - k11["Events::
EventBus


register_listener()
publisher()"]:::neighbor + k11["Events::
EventBus


register_listener(
const Events::EventChannel<Audio::PlaybackEnded>&
event,
Events::EventChannel<Audio::PlaybackEnded>::Callback
callback,
Events::EventBus::Delivery delivery,
Events::EventBus::ErrorHandler errors):
Events::EventBus::Registration

publisher(): Events::EventBus::Publisher"]:::neighbor k12["Audio::
PlaybackEnded"]:::focal k13(["Application
init_platform()"]):::app k14["Runtime::
RuntimeServices"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/catalog.json b/docs/site/engine-systems/diagrams/catalog.json index e541124e..92eaa143 100644 --- a/docs/site/engine-systems/diagrams/catalog.json +++ b/docs/site/engine-systems/diagrams/catalog.json @@ -13881,7 +13881,7 @@ }, { "name": "CE::Runtime::GameRuntime::shutdown", - "label": "shutdown()", + "label": "shutdown(): Runtime::ShutdownEndpoint", "file": "projects/engine/include/cheryl/core/game-framework/game-runtime.h", "line": 113, "brief": "Retain a copyable ordinary shutdown-request endpoint; use stop() or tick.request_stop() for unconditional shutdown.", @@ -13889,7 +13889,7 @@ }, { "name": "CE::Runtime::GameRuntime::lifecycle", - "label": "lifecycle()", + "label": "lifecycle(): Runtime::RuntimeLifecycle", "file": "projects/engine/include/cheryl/core/game-framework/game-runtime.h", "line": 112, "brief": "Retain synchronized live and terminal observations without borrowing GameRuntime.", @@ -13897,7 +13897,7 @@ }, { "name": "CE::Runtime::GameRuntime::set_shutdown_policy", - "label": "set_shutdown_policy()", + "label": "set_shutdown_policy(Runtime::ShutdownPolicyCallback callback, Runtime::ShutdownPolicyOptions options, Events::EventBus::Delivery delivery): Runtime::ShutdownPolicy", "file": "projects/engine/include/cheryl/core/game-framework/game-runtime.h", "line": 117, "brief": "Optionally install host shutdown negotiation before run(); retain the returned policy token while its callbacks are wanted.", @@ -13923,7 +13923,7 @@ }, { "name": "CE::Game::AbstractGame::update", - "label": "update(tick)", + "label": "update(const Game::TickContext& tick)", "file": "projects/engine/include/cheryl/core/game-framework/abstract-game.h", "line": 33, "brief": "Override to advance simulation using the borrowed TickContext; no native/GPU mutation from the concurrent simulation owner.", @@ -13931,7 +13931,7 @@ }, { "name": "CE::Game::AbstractGame::prepare_render_frame", - "label": "prepare_render_frame(frame)", + "label": "prepare_render_frame(Rendering::FrameWriter& frame)", "file": "projects/engine/include/cheryl/core/game-framework/abstract-game.h", "line": 34, "brief": "Override to copy render state into the lent frame writer; preparation may be skipped when no slot is free.", @@ -13951,7 +13951,7 @@ "Rendering::FrameWriter": [ { "name": "CE::Rendering::FrameWriter::begin_pass", - "label": "begin_pass()", + "label": "begin_pass(const glm::mat4& projection, const glm::mat4& view, Rendering::Pipelines::PassConstraints2D constraints, Rendering::Parameters::Set parameters): Rendering::PassWriter [borrowed]", "file": "projects/engine/include/cheryl/core/rendering/render-frame.h", "line": 132, "brief": "Copy camera matrices and pass options into a new retained render pass during this preparation call only.", @@ -13971,7 +13971,7 @@ "Runtime::ShutdownEndpoint": [ { "name": "CE::Runtime::ShutdownEndpoint::request", - "label": "request()", + "label": "request(Runtime::ShutdownReason reason): Runtime::ShutdownTicket", "file": "projects/engine/include/cheryl/core/game-framework/shutdown.h", "line": 76, "brief": "Stage an ordinary request for a safe platform pass; do not wait for its ticket on an owner needed by the policy.", @@ -13981,13 +13981,16 @@ "Runtime::RuntimeLifecycle": [ { "name": "CE::Runtime::RuntimeLifecycle::snapshot", - "label": "snapshot()", + "label": "snapshot(): Runtime::RuntimeLifecycleSnapshot", "file": "projects/engine/include/cheryl/core/game-framework/runtime-lifecycle.h", "line": 163, "brief": "Copy synchronized current/terminal lifecycle state from any thread, including after Runtime destruction.", "role": "call" } ] + }, + "operation_labels": { + "Application::GLFWOpenGL::make_context()": "Application::GLFWOpenGL::make_context(const Application::GLFWOpenGL::Options& config): Runtime::Context [unique]" } }, { @@ -14429,7 +14432,7 @@ "Runtime::Context": [ { "name": "CE::Runtime::Context::display", - "label": "display()", + "label": "display(): Platform::DisplaySystem&", "file": "projects/engine/include/cheryl/core/engine/engine-context.h", "line": 108, "brief": "Borrow monitor/window inventory on the platform owner.", @@ -14437,7 +14440,7 @@ }, { "name": "CE::Runtime::Context::window", - "label": "window()", + "label": "window(): Platform::Window&", "file": "projects/engine/include/cheryl/core/engine/engine-context.h", "line": 109, "brief": "Borrow the selected live window on platform; throws if none exists and does not marshal to that owner.", @@ -14445,7 +14448,7 @@ }, { "name": "CE::Runtime::Context::platform_dispatcher", - "label": "platform_dispatcher()", + "label": "platform_dispatcher(): Runtime::PlatformDispatcher&", "file": "projects/engine/include/cheryl/core/engine/engine-context.h", "line": 115, "brief": "Acquire a platform submission endpoint during initialization for later owner-affine window changes.", @@ -14455,17 +14458,17 @@ "Runtime::PlatformDispatcher::Submission": [ { "name": "CE::Runtime::PlatformDispatcher::Submission::submit", - "label": "submit()", + "label": "submit(Work&& work): Result [future]", "file": "projects/engine/include/cheryl/core/engine/platform-dispatcher.h", "line": 63, - "brief": "Queue owned window work for platform execution; never synchronously wait from that owner.", + "brief": "Queue an owned callable work(Runtime::Context&) for later platform execution. Result is the callable return type, delivered through its future; retain captures and never wait from the owner needed to execute the work.", "role": "call" } ], "Platform::Window": [ { "name": "CE::Platform::Window::resize", - "label": "resize()", + "label": "resize(width, height)", "file": "projects/engine/include/cheryl/core/display/window-interface.h", "line": 33, "brief": "Request a positive logical size on platform; runtime later copies observed dimensions into ticks.", @@ -14473,7 +14476,7 @@ }, { "name": "CE::Platform::Window::set_mode", - "label": "set_mode()", + "label": "set_mode(Platform::WindowMode mode)", "file": "projects/engine/include/cheryl/core/display/window-interface.h", "line": 34, "brief": "Request a backend-supported mode on the platform owner.", @@ -14481,7 +14484,7 @@ }, { "name": "CE::Platform::Window::hide_cursor", - "label": "hide_cursor()", + "label": "hide_cursor(hide)", "file": "projects/engine/include/cheryl/core/display/window-interface.h", "line": 35, "brief": "Change native cursor visibility on platform; this does not request relative pointer capture.", @@ -14491,7 +14494,7 @@ "Platform::DisplaySystem": [ { "name": "CE::Platform::DisplaySystem::monitors", - "label": "monitors()", + "label": "monitors(): const Platform::Monitor[]&", "file": "projects/engine/include/cheryl/core/display/display-system-interface.h", "line": 20, "brief": "Borrow current monitor inventory on platform; copy values before handing them to another thread.", @@ -14499,7 +14502,7 @@ }, { "name": "CE::Platform::DisplaySystem::primary_monitor", - "label": "primary_monitor()", + "label": "primary_monitor(): const Platform::Monitor&", "file": "projects/engine/include/cheryl/core/display/display-system-interface.h", "line": 22, "brief": "Borrow the current primary-monitor snapshot under the display inventory lifetime.", @@ -14507,7 +14510,7 @@ }, { "name": "CE::Platform::DisplaySystem::content_scale", - "label": "content_scale()", + "label": "content_scale(const Platform::Monitor& monitor)", "file": "projects/engine/include/cheryl/core/display/display-system-interface.h", "line": 24, "brief": "Read a recognized monitor snapshot's X/Y scale on platform; no automatic per-window scale following is implied.", @@ -14517,7 +14520,7 @@ "Rendering::Camera2D": [ { "name": "CE::Rendering::CameraBase::set_framebuffer_size", - "label": "set_framebuffer_size()", + "label": "set_framebuffer_size(Platform::FramebufferSize size)", "file": "projects/engine/include/cheryl/core/rendering/camera.h", "line": 22, "brief": "Inherited from CameraBase: apply tick.framebuffer_size on the camera's simulation owner; zero extent is retained.", @@ -15045,7 +15048,7 @@ "Runtime::Context": [ { "name": "CE::Runtime::Context::input", - "label": "input()", + "label": "input(): Input::System&", "file": "projects/engine/include/cheryl/core/engine/engine-context.h", "line": 113, "brief": "Borrow the initialized input adapter during init_platform(); access does not marshal to platform.", @@ -15055,7 +15058,7 @@ "Input::System": [ { "name": "CE::Input::System::bindings", - "label": "bindings()", + "label": "bindings(): Input::Actions::Bindings&", "file": "projects/engine/include/cheryl/core/controls/input-interface.h", "line": 36, "brief": "Borrow mutable direct-action bindings on platform; do not mutate from concurrent simulation.", @@ -15063,7 +15066,7 @@ }, { "name": "CE::Input::System::action_contexts", - "label": "action_contexts()", + "label": "action_contexts(): Input::Actions::ContextSubmission", "file": "projects/engine/include/cheryl/core/controls/input-interface.h", "line": 38, "brief": "Acquire a retained request endpoint on platform; later attach() calls can run on simulation or other producers.", @@ -15071,7 +15074,7 @@ }, { "name": "CE::Input::System::keyboard_id", - "label": "keyboard_id()", + "label": "keyboard_id(): Input::DeviceId", "file": "projects/engine/include/cheryl/core/controls/input-interface.h", "line": 40, "brief": "Read the keyboard identity on platform and retain that value for controller definitions.", @@ -15081,23 +15084,23 @@ "Input::Actions::Bindings": [ { "name": "CE::Input::Actions::Bindings::bind_button", - "label": "bind_button()", + "label": "bind_button(Input::Control control, Input::Actions::Id action): Input::Actions::BindingId", "file": "projects/engine/include/cheryl/core/controls/input-bindings.h", "line": 93, - "brief": "Map a physical control or chord to a direct action on platform; retain the returned token.", + "brief": "Map a physical control or chord to a direct action on platform; retain the returned BindingId. The chord overload accepts Input::Actions::Chord chord instead of Input::Control control.", "role": "call" }, { "name": "CE::Input::Actions::Bindings::bind_axis", - "label": "bind_axis()", + "label": "bind_axis(Input::Control axis, Input::Actions::Id action, Input::Actions::AxisOptions options): Input::Actions::BindingId", "file": "projects/engine/include/cheryl/core/controls/input-bindings.h", "line": 97, - "brief": "Map a physical axis with optional modifiers, scale and dead zone on platform.", + "brief": "Map a physical axis with optional modifiers, scale and dead zone on platform. The modified-axis overload additionally accepts Input::Actions::Chord modifiers before axis; retain the returned BindingId.", "role": "call" }, { "name": "CE::Input::Actions::Bindings::unbind", - "label": "unbind()", + "label": "unbind(Input::Actions::BindingId binding)", "file": "projects/engine/include/cheryl/core/controls/input-bindings.h", "line": 99, "brief": "Remove a direct binding by its retained token on platform during cleanup.", @@ -15107,7 +15110,7 @@ "Input::Actions::ContextSubmission": [ { "name": "CE::Input::Actions::ContextSubmission::attach", - "label": "attach(Input::Actions::ContextDefinition)", + "label": "attach(Input::Actions::ContextDefinition definition): Input::Actions::Context", "file": "projects/engine/include/cheryl/core/controls/action-context.h", "line": 72, "brief": "Accept an owned Input::Actions::ContextDefinition and return an Input::Actions::Context lease; mappings apply at the next input poll, not immediately.", @@ -15117,7 +15120,7 @@ "Input::Actions::Context": [ { "name": "CE::Input::Actions::Context::id", - "label": "id()", + "label": "id(): Input::Actions::ContextId", "file": "projects/engine/include/cheryl/core/controls/action-context.h", "line": 57, "brief": "Copy namespace identity if a retained historical action view should outlive the lease.", @@ -15135,15 +15138,15 @@ "Input::Tick": [ { "name": "CE::Input::Tick::actions", - "label": "actions(Input::Actions::Context)", + "label": "actions(const Input::Actions::Context& context): Input::Actions::View [borrowed]", "file": "projects/engine/include/cheryl/core/controls/tick-input.h", "line": 74, - "brief": "Select one controller's namespace using its Input::Actions::Context lease; the ContextId overload accepts the copied namespace identity. The returned View borrows this update's Tick.", + "brief": "Select one controller's namespace using its Input::Actions::Context lease; the ContextId overload accepts the copied namespace identity. The returned View borrows this update's Tick. ContextDefinition is supplied to attach(), not actions().", "role": "call" }, { "name": "CE::Input::Tick::button", - "label": "button()", + "label": "button(Input::Actions::Id action): Input::Actions::ButtonTickState", "file": "projects/engine/include/cheryl/core/controls/tick-input.h", "line": 71, "brief": "Read direct/global button activity and observation durations without polling live devices.", @@ -15151,7 +15154,7 @@ }, { "name": "CE::Input::Tick::axis", - "label": "axis()", + "label": "axis(Input::Actions::Id action): Input::Actions::AxisTickState", "file": "projects/engine/include/cheryl/core/controls/tick-input.h", "line": 72, "brief": "Read direct/global absolute values or accumulated relative deltas.", @@ -15161,7 +15164,7 @@ "Input::Actions::View": [ { "name": "CE::Input::Actions::View::button", - "label": "button()", + "label": "button(Input::Actions::Id action): Input::Actions::ButtonTickState", "file": "projects/engine/include/cheryl/core/controls/tick-input.h", "line": 116, "brief": "Read a local button in the selected context; keep the borrowed Tick alive and unmoved.", @@ -15169,7 +15172,7 @@ }, { "name": "CE::Input::Actions::View::axis", - "label": "axis()", + "label": "axis(Input::Actions::Id action): Input::Actions::AxisTickState", "file": "projects/engine/include/cheryl/core/controls/tick-input.h", "line": 117, "brief": "Read a local axis in the selected context; missing actions return inactive/zero state.", @@ -15547,7 +15550,7 @@ "Assets::Loading::Loader": [ { "name": "CE::Assets::Loading::Loader::load_assets", - "label": "load_assets()", + "label": "load_assets(Rendering::Resources::Provider& provider)", "file": "projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h", "line": 115, "brief": "Prepare and upload synchronously on the active provider/loading owner; partial publication is possible on failure.", @@ -15555,7 +15558,7 @@ }, { "name": "CE::Assets::Loading::Loader::prepare", - "label": "prepare()", + "label": "prepare(): Assets::Loading::Prepared", "file": "projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h", "line": 112, "brief": "Own a CPU-only batch without publishing caches; the application may run this on a worker.", @@ -15563,7 +15566,7 @@ }, { "name": "CE::Assets::Loading::Loader::upload", - "label": "upload()", + "label": "upload(Assets::Loading::Prepared prepared, Rendering::Resources::Provider& provider, replace_shaders)", "file": "projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h", "line": 114, "brief": "Consume the prepared batch on the active provider/loading owner. Successful entries can publish before the whole batch completes.", @@ -15573,7 +15576,7 @@ "Assets::Sprites::Manager": [ { "name": "CE::Assets::Sprites::Manager::get_asset", - "label": "get_asset()", + "label": "get_asset(name_space, name): Assets::Sprites::Manager::Handle [shared]", "file": "projects/engine/include/cheryl/core/resources/asset-management/sprite-mgr.h", "line": 30, "brief": "Look up an exact namespace/name pair; returns a retained asset or null without loading.", @@ -15583,7 +15586,7 @@ "Assets::Tilesets::Manager": [ { "name": "CE::Assets::Cache::Store::get_asset", - "label": "get_asset()", + "label": "get_asset(key): Assets::Tilesets::Manager::Handle [shared]", "file": "projects/engine/include/cheryl/templates/asset-mgr.h", "line": 149, "brief": "Inherited Store lookup of an exact qualified namespace:name key. Returns a retained asset or null without loading.", @@ -15593,7 +15596,7 @@ "Assets::Sprites::Asset": [ { "name": "CE::Assets::Sprites::Asset::animation", - "label": "animation()", + "label": "animation(animation, facing): Assets::Sprites::Animation", "file": "projects/engine/include/cheryl/assets/types/2d/sprite.h", "line": 62, "brief": "Create independent CPU playback from an exact clip name and optional facing; missing or ambiguous selection throws.", @@ -15603,7 +15606,7 @@ "Assets::Tilesets::Asset": [ { "name": "CE::Assets::Tilesets::Asset::tile", - "label": "tile()", + "label": "tile(cell): Assets::Tilesets::Tile", "file": "projects/engine/include/cheryl/assets/types/2d/tileset.h", "line": 78, "brief": "Retain one static grid cell and its resources; invalid cell selection throws.", @@ -15611,7 +15614,7 @@ }, { "name": "CE::Assets::Tilesets::Asset::animation", - "label": "animation()", + "label": "animation(name): Assets::Tilesets::Animation", "file": "projects/engine/include/cheryl/assets/types/2d/tileset.h", "line": 79, "brief": "Create an independent cursor for a named tile animation.", @@ -15619,7 +15622,7 @@ }, { "name": "CE::Assets::Tilesets::Asset::cell_at", - "label": "cell_at()", + "label": "cell_at(Assets::Grids::CellIndex target, elapsed): Assets::Grids::CellIndex", "file": "projects/engine/include/cheryl/assets/types/2d/tileset.h", "line": 85, "brief": "Resolve one original target at nonnegative elapsed milliseconds; this neither advances shared data nor recursively animates the result.", @@ -15629,7 +15632,7 @@ "Assets::Sprites::Animation": [ { "name": "CE::Assets::Sprites::Animation::advance", - "label": "advance()", + "label": "advance(elapsed)", "file": "projects/engine/include/cheryl/assets/types/2d/sprite.h", "line": 35, "brief": "Advance the independent cursor on its simulation owner using finite nonnegative elapsed seconds.", @@ -15637,13 +15640,16 @@ }, { "name": "CE::Assets::Sprites::Animation::cell", - "label": "cell()", + "label": "cell(): Assets::Grids::CellIndex", "file": "projects/engine/include/cheryl/assets/types/2d/sprite.h", "line": 37, "brief": "Read the current grid cell for draw submission; it differs from the clip-frame ordinal.", "role": "call" } ] + }, + "operation_labels": { + "Rendering::Submission::resolve_sprite()": "Rendering::Submission::resolve_sprite(const Assets::Sprites::Asset& sprite, Assets::Grids::CellIndex cell, const Rendering::DrawStyle2D& style, const Rendering::Submission::Context2D& context): Rendering::DrawPacket2D" } }, { @@ -16131,7 +16137,7 @@ "Runtime::Context": [ { "name": "CE::Runtime::Context::resources", - "label": "resources()", + "label": "resources(): Rendering::Resources::Provider&", "file": "projects/engine/include/cheryl/core/engine/engine-context.h", "line": 112, "brief": "Borrow the selected resource provider. The getter does not marshal calls to its graphics owner.", @@ -16141,7 +16147,7 @@ "Rendering::Resources::Provider": [ { "name": "CE::Rendering::Resources::Provider::create_image", - "label": "create_image()", + "label": "create_image(const Assets::Images::Decoded& image): Rendering::Resources::Image [shared]", "file": "projects/engine/include/cheryl/assets/resources/resource-provider.h", "line": 66, "brief": "Copy owned decoded RGBA pixels into a fresh immutable image on the backend upload owner/current context.", @@ -16149,15 +16155,15 @@ }, { "name": "CE::Rendering::Resources::Provider::upload_geometry", - "label": "upload_geometry()", + "label": "upload_geometry(const Rendering::Geometry::Vertex2D[] vertices [borrowed], Rendering::Geometry::Topology topology): Rendering::Geometry::Buffer2D [shared]", "file": "projects/engine/include/cheryl/assets/resources/resource-provider.h", "line": 77, - "brief": "Copy transient vertices into immutable geometry on the backend upload owner/current context.", + "brief": "Upload the ordinary Vertex2D layout on the active provider owner. Separate overloads accept Vertex2DColor[] or a compatibility shared vertex owner.", "role": "call" }, { "name": "CE::Rendering::Resources::Provider::graphics_domain", - "label": "graphics_domain()", + "label": "graphics_domain(): Rendering::GraphicsDomain", "file": "projects/engine/include/cheryl/assets/resources/resource-provider.h", "line": 59, "brief": "Copy the retained graphics-domain observation token; empty means unsupported.", @@ -16167,13 +16173,16 @@ "Rendering::GraphicsDomain": [ { "name": "CE::Rendering::GraphicsDomain::snapshot", - "label": "snapshot()", + "label": "snapshot(): Rendering::GraphicsDomainSnapshot", "file": "projects/engine/include/cheryl/core/rendering/graphics-domain.h", "line": 47, "brief": "Copy native-domain identity, sequence and usability from any thread without retaining native objects.", "role": "call" } ] + }, + "operation_labels": { + "Assets::Images::decode()": "Assets::Images::decode(file): Assets::Images::Decoded" } }, { @@ -16480,7 +16489,7 @@ "Rendering::Camera2D": [ { "name": "CE::Rendering::CameraBase::set_framebuffer_size", - "label": "set_framebuffer_size()", + "label": "set_framebuffer_size(Platform::FramebufferSize size)", "file": "projects/engine/include/cheryl/core/rendering/camera.h", "line": 22, "brief": "Inherited from CameraBase. Update framebuffer pixel extent and projection on the camera owner.", @@ -16488,7 +16497,7 @@ }, { "name": "CE::Rendering::CameraBase::set_view_matrix", - "label": "set_view_matrix()", + "label": "set_view_matrix(const glm::mat4& view)", "file": "projects/engine/include/cheryl/core/rendering/camera.h", "line": 25, "brief": "Inherited from CameraBase. Copy a world-to-view transform on the camera owner.", @@ -16496,7 +16505,7 @@ }, { "name": "CE::Rendering::CameraBase::projection_matrix", - "label": "projection_matrix()", + "label": "projection_matrix(): const glm::mat4&", "file": "projects/engine/include/cheryl/core/rendering/camera.h", "line": 28, "brief": "Inherited from CameraBase. Borrow the projection matrix to copy into begin_pass(); exclude concurrent camera mutation.", @@ -16504,7 +16513,7 @@ }, { "name": "CE::Rendering::CameraBase::view_matrix", - "label": "view_matrix()", + "label": "view_matrix(): const glm::mat4&", "file": "projects/engine/include/cheryl/core/rendering/camera.h", "line": 29, "brief": "Inherited from CameraBase. Borrow the view matrix to copy into begin_pass(); exclude concurrent camera mutation.", @@ -16514,7 +16523,7 @@ "Rendering::FrameWriter": [ { "name": "CE::Rendering::FrameWriter::begin_pass", - "label": "begin_pass()", + "label": "begin_pass(const glm::mat4& projection, const glm::mat4& view, Rendering::Pipelines::PassConstraints2D constraints, Rendering::Parameters::Set parameters): Rendering::PassWriter [borrowed]", "file": "projects/engine/include/cheryl/core/rendering/render-frame.h", "line": 132, "brief": "Append a pass with copied camera matrices, constraints and pass parameters during frame preparation.", @@ -16524,15 +16533,15 @@ "Rendering::PassWriter": [ { "name": "CE::Rendering::PassWriter::add", - "label": "add()", + "label": "add(Rendering::DrawPacket2D draw)", "file": "projects/engine/include/cheryl/core/rendering/render-frame.h", "line": 88, - "brief": "Validate and append one packet or a packet list in authored draw order; the writer is borrowed only during frame preparation.", + "brief": "Validate and append one retained packet. A separate batch overload accepts DrawPacket2D[]; packet order is preserved.", "role": "call" }, { "name": "CE::Rendering::PassWriter::semantics", - "label": "semantics()", + "label": "semantics(): Rendering::Parameters::PassSemantics", "file": "projects/engine/include/cheryl/core/rendering/render-frame.h", "line": 78, "brief": "Copy this pass projection/view semantics for submission resolution.", @@ -16540,7 +16549,7 @@ }, { "name": "CE::Rendering::PassWriter::parameters", - "label": "parameters()", + "label": "parameters(): const Rendering::Parameters::Set&", "file": "projects/engine/include/cheryl/core/rendering/render-frame.h", "line": 83, "brief": "Borrow this pass parameter set while creating the submission Context2D; outer frame growth can invalidate references.", @@ -16548,13 +16557,16 @@ }, { "name": "CE::Rendering::PassWriter::constraints", - "label": "constraints()", + "label": "constraints(): const Rendering::Pipelines::PassConstraints2D&", "file": "projects/engine/include/cheryl/core/rendering/render-frame.h", "line": 84, "brief": "Borrow this pass pipeline constraints to copy into the submission Context2D; keep the frame stable while copying.", "role": "call" } ] + }, + "operation_labels": { + "Rendering::Submission::resolve_sprite()": "Rendering::Submission::resolve_sprite(const Assets::Sprites::Asset& sprite, Assets::Grids::CellIndex cell, const Rendering::DrawStyle2D& style, const Rendering::Submission::Context2D& context): Rendering::DrawPacket2D" } }, { @@ -17152,7 +17164,7 @@ "Runtime::Context": [ { "name": "CE::Runtime::Context::fonts", - "label": "fonts()", + "label": "fonts(): Text::FontService&", "file": "projects/engine/include/cheryl/core/engine/engine-context.h", "line": 114, "brief": "Borrow the Context CPU font catalogue; configure before constructing consumers.", @@ -17162,15 +17174,15 @@ "Text::FontService": [ { "name": "CE::Text::FontService::configure", - "label": "configure()", + "label": "configure(const Text::FontConfiguration& configuration)", "file": "projects/engine/include/cheryl/text/font-service.h", "line": 51, - "brief": "Build and publish the catalogue before consumers are created; existing retained fonts and toolkit consumers stay unchanged.", + "brief": "Configure the full font catalogue before constructing consumers. The API also offers FontSelection overloads; existing handles keep their previous snapshots.", "role": "call" }, { "name": "CE::Text::FontService::default_font", - "label": "default_font()", + "label": "default_font(): Text::Font", "file": "projects/engine/include/cheryl/text/font-service.h", "line": 54, "brief": "Retain the configured default, or embedded fallback when the default is unavailable.", @@ -17178,7 +17190,7 @@ }, { "name": "CE::Text::FontService::get", - "label": "get()", + "label": "get(reference): Text::Font", "file": "projects/engine/include/cheryl/text/font-service.h", "line": 55, "brief": "Look up an exact application font reference; missing references return the default rather than an empty handle.", @@ -17198,7 +17210,7 @@ "Text::ShapedText": [ { "name": "CE::Text::ShapedText::glyphs", - "label": "glyphs()", + "label": "glyphs(): const Text::ShapedGlyph[] [borrowed]", "file": "projects/engine/include/cheryl/text/layout.h", "line": 78, "brief": "Inspect visually ordered glyphs and logical shaping-cluster ranges before upload; retain the unmoved layout.", @@ -17206,7 +17218,7 @@ }, { "name": "CE::Text::ShapedText::lines", - "label": "lines()", + "label": "lines(): const Text::TextLine[] [borrowed]", "file": "projects/engine/include/cheryl/text/layout.h", "line": 79, "brief": "Inspect accepted line records and glyph ranges before upload; retain the unmoved layout.", @@ -17216,7 +17228,7 @@ "Text::Rendering::Updater": [ { "name": "CE::Text::Rendering::Updater::request", - "label": "request()", + "label": "request(Text::Rendering::Request next)", "file": "projects/engine/include/cheryl/text/text-updater.h", "line": 53, "brief": "Own the latest desired content/options on the serialized simulation owner. Work starts only when update() runs.", @@ -17232,13 +17244,19 @@ }, { "name": "CE::Text::Rendering::Updater::current", - "label": "current()", + "label": "current(): const Text::Rendering::Generation [shared]", "file": "projects/engine/include/cheryl/text/text-updater.h", "line": 57, "brief": "Retain the last complete generation for frame preparation. The empty constructor has no current generation until an update succeeds.", "role": "call" } ] + }, + "operation_labels": { + "Text::layout_text()": "Text::layout_text(const Text::Font& font, text, const Text::LayoutOptions& options): Text::ShapedText", + "Text::Rendering::prepare()": "Text::Rendering::prepare(Text::ShapedText layout, Text::Rendering::RasterOptions options): Text::Rendering::Prepared", + "Text::Rendering::upload()": "Text::Rendering::upload(Text::Rendering::Prepared prepared, Rendering::Resources::Provider& provider, const Rendering::Resources::Material material [optional shared]): Text::Rendering::Generation", + "Rendering::Submission::resolve_text()": "Rendering::Submission::resolve_text(const Text::Rendering::Generation& text, const Rendering::DrawStyle2D& style, const Rendering::Submission::Context2D& context): Rendering::DrawPacket2D[]" } }, { @@ -17859,7 +17877,7 @@ "UI::TGUI::Session": [ { "name": "CE::UI::TGUI::Session::gui", - "label": "gui()", + "label": "gui(): tgui::BackendGui&", "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/session.h", "line": 72, "brief": "Borrow the native toolkit authoring interface on the UI owner; release native references before session closure.", @@ -17867,7 +17885,7 @@ }, { "name": "CE::UI::TGUI::Session::handle_input", - "label": "handle_input()", + "label": "handle_input(const Input::Events::Record[] records [borrowed], pointer_selected)", "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/session.h", "line": 95, "brief": "Feed the complete ordered input batch once on the UI owner; the application selects pointer delivery.", @@ -17875,7 +17893,7 @@ }, { "name": "CE::UI::TGUI::Session::request_keyboard_focus", - "label": "request_keyboard_focus()", + "label": "request_keyboard_focus(Input::Routing::KeyboardPolicy routing)", "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/session.h", "line": 89, "brief": "Request session keyboard/text routing for future input polls; native widget or element focus remains separate.", @@ -17883,7 +17901,7 @@ }, { "name": "CE::UI::TGUI::Session::record", - "label": "record()", + "label": "record(): UI::TGUI::RecordedScene", "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/session.h", "line": 104, "brief": "Create a complete owned CPU recording on the UI owner, without graphics upload.", @@ -17893,25 +17911,25 @@ "UI::TGUI::SceneUploader": [ { "name": "CE::UI::TGUI::SceneUploader::upload", - "label": "upload()", + "label": "upload(Rendering::Resources::Provider& provider, const UI::TGUI::RecordedScene& recording): UI::TGUI::Scene", "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/scene.h", "line": 59, - "brief": "Upload a complete CPU recording directly on the original provider owner/current context.", + "brief": "Return an immutable scene on the provider owner using lazy builtin UI materials. A separate overload accepts explicit Materials overrides.", "role": "call" }, { "name": "CE::UI::TGUI::SceneUploader::submit", - "label": "submit()", + "label": "submit(const Runtime::PlatformDispatcher::Submission& platform, UI::TGUI::RecordedScene recording): UI::TGUI::Scene [future]", "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/scene.h", "line": 63, - "brief": "Submit an owned recording through the matching platform endpoint; no live toolkit object crosses the boundary. Adopt the ready scene explicitly.", + "brief": "Return a future for a complete uploaded scene through the platform submission endpoint; adopt it without blocking. A separate overload accepts explicit Materials overrides.", "role": "call" } ], "UI::TGUI::Scene": [ { "name": "CE::UI::TGUI::Scene::write", - "label": "write()", + "label": "write(Rendering::FrameWriter& frame)", "file": "projects/modules/ui/tgui/include/cheryl/ui/tgui/scene.h", "line": 43, "brief": "Append the retained complete scene to the borrowed FrameWriter at the application-selected pass position; no toolkit or upload calls occur.", @@ -17921,7 +17939,7 @@ "UI::RmlUi::Session": [ { "name": "CE::UI::RmlUi::Session::context", - "label": "context()", + "label": "context(): Rml::Context&", "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/session.h", "line": 74, "brief": "Borrow the native toolkit authoring interface on the UI owner; release native references before session closure.", @@ -17929,7 +17947,7 @@ }, { "name": "CE::UI::RmlUi::Session::handle_input", - "label": "handle_input()", + "label": "handle_input(const Input::Events::Record[] records [borrowed], pointer_selected)", "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/session.h", "line": 93, "brief": "Feed the complete ordered input batch once on the UI owner; the application selects pointer delivery.", @@ -17937,7 +17955,7 @@ }, { "name": "CE::UI::RmlUi::Session::request_keyboard_focus", - "label": "request_keyboard_focus()", + "label": "request_keyboard_focus(Input::Routing::KeyboardPolicy routing)", "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/session.h", "line": 90, "brief": "Request session keyboard/text routing for future input polls; native widget or element focus remains separate.", @@ -17945,7 +17963,7 @@ }, { "name": "CE::UI::RmlUi::Session::record", - "label": "record()", + "label": "record(): UI::RmlUi::RecordedScene", "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/session.h", "line": 102, "brief": "Create a complete owned CPU recording on the UI owner, without graphics upload.", @@ -17955,25 +17973,25 @@ "UI::RmlUi::SceneUploader": [ { "name": "CE::UI::RmlUi::SceneUploader::upload", - "label": "upload()", + "label": "upload(Rendering::Resources::Provider& provider, const UI::RmlUi::RecordedScene& recording): UI::RmlUi::Scene", "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/scene.h", "line": 54, - "brief": "Upload a complete CPU recording directly on the original provider owner/current context.", + "brief": "Return an immutable scene on the provider owner using lazy builtin UI materials. A separate overload accepts explicit Materials overrides.", "role": "call" }, { "name": "CE::UI::RmlUi::SceneUploader::submit", - "label": "submit()", + "label": "submit(const Runtime::PlatformDispatcher::Submission& platform, UI::RmlUi::RecordedScene recording): UI::RmlUi::Scene [future]", "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/scene.h", "line": 58, - "brief": "Submit an owned recording through the matching platform endpoint; no live toolkit object crosses the boundary. Adopt the ready scene explicitly.", + "brief": "Return a future for a complete uploaded scene through the platform submission endpoint; adopt it without blocking. A separate overload accepts explicit Materials overrides.", "role": "call" } ], "UI::RmlUi::Scene": [ { "name": "CE::UI::RmlUi::Scene::write", - "label": "write()", + "label": "write(Rendering::FrameWriter& frame)", "file": "projects/modules/ui/rmlui/include/cheryl/ui/rmlui/scene.h", "line": 39, "brief": "Append the retained complete scene to the borrowed FrameWriter at the application-selected pass position; no toolkit or upload calls occur.", @@ -18459,7 +18477,7 @@ "Audio::Miniaudio::System": [ { "name": "CE::Audio::Miniaudio::System::open_device", - "label": "open_device()", + "label": "open_device(Audio::Format output, Audio::PlaybackNotificationOptions notifications): Audio::Miniaudio::System [unique]", "file": "projects/modules/audio/miniaudio/include/cheryl/backends/miniaudio.h", "line": 43, "brief": "Open native output at a loading boundary; preparation may block. Playback then advances on the device clock.", @@ -18467,7 +18485,7 @@ }, { "name": "CE::Audio::Miniaudio::System::publish_events_to", - "label": "publish_events_to()", + "label": "publish_events_to(Events::EventBus::Publisher publisher)", "file": "projects/modules/audio/miniaudio/include/cheryl/backends/miniaudio.h", "line": 56, "brief": "Select a weak event publication route on the designated notification owner before pumping.", @@ -18475,7 +18493,7 @@ }, { "name": "CE::Audio::Miniaudio::System::pump_with", - "label": "pump_with()", + "label": "pump_with(const Runtime::RuntimeServices& services): Runtime::ServiceAttachment", "file": "projects/modules/audio/miniaudio/include/cheryl/backends/miniaudio.h", "line": 59, "brief": "Optionally attach notification publication to platform maintenance on its initialized owner. Retain the attachment; it neither maintains voices nor advances audio time.", @@ -18493,7 +18511,7 @@ "Audio::Clip": [ { "name": "CE::Audio::Clip::from_samples", - "label": "from_samples()", + "label": "from_samples(Audio::Format format, samples): Audio::Clip", "file": "projects/engine/include/cheryl/audio/clip.h", "line": 38, "brief": "Create immutable owned interleaved float PCM without opening a device; decoding an audio file uses Miniaudio::decode_file() instead.", @@ -18503,7 +18521,7 @@ "Audio::System": [ { "name": "CE::Audio::System::play", - "label": "play()", + "label": "play(const Audio::Clip& clip, Audio::PlaybackOptions options): Audio::Voice [shared]", "file": "projects/engine/include/cheryl/audio/system.h", "line": 75, "brief": "Admit an independent voice retaining the prepared clip; dropping the voice handle does not stop playback.", @@ -18511,7 +18529,7 @@ }, { "name": "CE::Audio::System::stream", - "label": "stream()", + "label": "stream(file, Audio::PlaybackOptions options): Audio::Voice [shared]", "file": "projects/engine/include/cheryl/audio/system.h", "line": 77, "brief": "Open an independent streamed voice; keep its source file available through playback and restart.", @@ -18537,7 +18555,7 @@ "Audio::Voice": [ { "name": "CE::Audio::Voice::snapshot", - "label": "snapshot()", + "label": "snapshot(): Audio::VoiceSnapshot", "file": "projects/engine/include/cheryl/audio/system.h", "line": 52, "brief": "Copy synchronized voice state and current-generation outcome; surviving handles report Closed after output closure.", @@ -18571,7 +18589,7 @@ "Events::EventBus": [ { "name": "CE::Events::EventBus::register_listener", - "label": "register_listener()", + "label": "register_listener(const Events::EventChannel& event, Events::EventChannel::Callback callback, Events::EventBus::Delivery delivery, Events::EventBus::ErrorHandler errors): Events::EventBus::Registration", "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", "line": 206, "brief": "Register a persistent typed PlaybackEnded listener. Immediate delivery runs on the notification-pump owner; use an explicit delivery adapter for simulation-owned state.", @@ -18579,7 +18597,7 @@ }, { "name": "CE::Events::EventBus::publisher", - "label": "publisher()", + "label": "publisher(): Events::EventBus::Publisher", "role": "call", "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", "line": 184, @@ -18596,6 +18614,9 @@ "role": "call" } ] + }, + "operation_labels": { + "Audio::Miniaudio::decode_file()": "Audio::Miniaudio::decode_file(file, Audio::Miniaudio::DecodeOptions options): Audio::Clip" } }, { @@ -19048,7 +19069,7 @@ "Runtime::Context": [ { "name": "CE::Runtime::Context::events", - "label": "events()", + "label": "events(): Events::EventBus&", "file": "projects/engine/include/cheryl/core/engine/engine-context.h", "line": 116, "brief": "Borrow this context's bus for its runtime-connected producer observations; raw GLFW resize uses a separate default bus.", @@ -19058,7 +19079,7 @@ "Events::EventBus::Publisher": [ { "name": "CE::Events::EventBus::Publisher::notify", - "label": "notify()", + "label": "notify(const Events::EventChannel& event, Value&& payload, const Events::EventBus::ErrorHandler& errors)", "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", "line": 167, "brief": "Publish an exact-type observation without propagating notification failures; this weak endpoint retains no bus or Context.", @@ -19068,7 +19089,7 @@ "Events::EventBus": [ { "name": "CE::Events::EventBus::register_listener", - "label": "register_listener()", + "label": "register_listener(const Events::EventChannel& event, Events::EventChannel::Callback callback, Events::EventBus::Delivery delivery, Events::EventBus::ErrorHandler errors): Events::EventBus::Registration", "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", "line": 206, "brief": "Register an exact channel and retain the persistent registration; optional queued delivery requires an independent, nonthrowing error sink.", @@ -19076,7 +19097,7 @@ }, { "name": "CE::Events::EventBus::publisher", - "label": "publisher()", + "label": "publisher(): Events::EventBus::Publisher", "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", "line": 184, "brief": "Acquire a copyable weak route for producers without retaining the bus facade.", @@ -19084,7 +19105,7 @@ }, { "name": "CE::Events::EventBus::unregister_and_wait", - "label": "unregister_and_wait()", + "label": "unregister_and_wait(const Events::EventBus::Registration& registration)", "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", "line": 249, "brief": "Invalidate and wait for entered callbacks before destroying borrowed targets; never wait from that listener's own callback.", @@ -19101,6 +19122,9 @@ "role": "call" } ] + }, + "operation_labels": { + "Events::simulation_delivery()": "Events::simulation_delivery(Runtime::SimulationDispatcher::Submission endpoint): Events::EventBus::Delivery" } }, { @@ -19490,7 +19514,7 @@ "Runtime::Context": [ { "name": "CE::Runtime::Context::platform_dispatcher", - "label": "platform_dispatcher()", + "label": "platform_dispatcher(): Runtime::PlatformDispatcher&", "file": "projects/engine/include/cheryl/core/engine/engine-context.h", "line": 115, "brief": "Acquire the runtime-controlled platform queue facade while Context lives.", @@ -19498,7 +19522,7 @@ }, { "name": "CE::Runtime::Context::resources", - "label": "resources()", + "label": "resources(): Rendering::Resources::Provider&", "file": "projects/engine/include/cheryl/core/engine/engine-context.h", "line": 112, "brief": "Borrow compatible resource upload services inside platform-owned work with the graphics context current.", @@ -19508,7 +19532,7 @@ "Runtime::PlatformDispatcher": [ { "name": "CE::Runtime::PlatformDispatcher::submission", - "label": "submission()", + "label": "submission(): Runtime::PlatformDispatcher::Submission", "file": "projects/engine/include/cheryl/core/engine/platform-dispatcher.h", "line": 95, "brief": "Retain a copyable endpoint while the dispatcher lives; acquiring it does not open queue acceptance.", @@ -19518,17 +19542,17 @@ "Runtime::PlatformDispatcher::Submission": [ { "name": "CE::Runtime::PlatformDispatcher::Submission::submit", - "label": "submit()", + "label": "submit(Work&& work): Result [future]", "file": "projects/engine/include/cheryl/core/engine/platform-dispatcher.h", "line": 63, - "brief": "Queue owned work(Context&) for later platform execution; retain its future and adopt only after readiness.", + "brief": "Queue an owned callable work(Runtime::Context&) for later platform execution. Result is the callable return type, delivered through its future; retain captures and never wait from the owner needed to execute the work.", "role": "call" } ], "Rendering::Resources::Provider": [ { "name": "CE::Rendering::Resources::Provider::create_image", - "label": "create_image()", + "label": "create_image(const Assets::Images::Decoded& image): Rendering::Resources::Image [shared]", "file": "projects/engine/include/cheryl/assets/resources/resource-provider.h", "line": 66, "brief": "Upload owned decoded pixels on the backend's graphics/loading owner; the returned immutable image retains no input buffer.", @@ -19536,17 +19560,17 @@ }, { "name": "CE::Rendering::Resources::Provider::upload_geometry", - "label": "upload_geometry()", + "label": "upload_geometry(const Rendering::Geometry::Vertex2D[] vertices [borrowed], Rendering::Geometry::Topology topology): Rendering::Geometry::Buffer2D [shared]", "file": "projects/engine/include/cheryl/assets/resources/resource-provider.h", "line": 77, - "brief": "Upload transient vertices on the backend owner and retain the returned geometry handle.", + "brief": "Upload transient vertices on the backend owner and retain the returned geometry handle. The API also accepts a Vertex2DColor collection or a shared Vertex2D buffer plus vertex_count. Diagram collection/result notation omits standard-library ownership wrappers.", "role": "call" } ], "Runtime::GameRuntime": [ { "name": "CE::Runtime::GameRuntime::simulation_dispatcher", - "label": "simulation_dispatcher()", + "label": "simulation_dispatcher(): Runtime::SimulationDispatcher&", "file": "projects/engine/include/cheryl/core/game-framework/game-runtime.h", "line": 119, "brief": "Acquire the runtime-controlled simulation queue before producers need to post owner work.", @@ -19556,7 +19580,7 @@ "Runtime::SimulationDispatcher": [ { "name": "CE::Runtime::SimulationDispatcher::submission", - "label": "submission()", + "label": "submission(): Runtime::SimulationDispatcher::Submission", "file": "projects/engine/include/cheryl/core/engine/simulation-dispatcher.h", "line": 95, "brief": "Retain a producer endpoint independent of dispatcher lifetime; runtime opens/closes the session queue.", @@ -19566,10 +19590,10 @@ "Runtime::SimulationDispatcher::Submission": [ { "name": "CE::Runtime::SimulationDispatcher::Submission::submit", - "label": "submit()", + "label": "submit(Work&& work): Result [future]", "file": "projects/engine/include/cheryl/core/engine/simulation-dispatcher.h", "line": 63, - "brief": "Queue owned work() for a simulation update boundary; never wait for the result on simulation.", + "brief": "Queue an owned callable work() for a simulation update boundary. Result is the callable return type, delivered through its future; never wait from simulation.", "role": "call" } ] @@ -19936,7 +19960,7 @@ "Workers::Pool": [ { "name": "CE::Workers::Pool::make_group", - "label": "make_group()", + "label": "make_group(Workers::GroupOptions options): Workers::Group", "file": "projects/engine/include/cheryl/core/engine/worker-pool.h", "line": 200, "brief": "Create a standalone service workload on this shared physical pool; use Context::make_worker_group() for runtime-tracked work.", @@ -19954,7 +19978,7 @@ "Runtime::Context": [ { "name": "CE::Runtime::Context::make_worker_group", - "label": "make_worker_group()", + "label": "make_worker_group(Workers::GroupOptions options): Workers::Group", "file": "projects/engine/include/cheryl/core/engine/engine-context.h", "line": 124, "brief": "Create a runtime-tracked CPU group on lazy owned or configured shared capacity; Runtime settles these groups before game resources are released.", @@ -19964,10 +19988,10 @@ "Workers::Group": [ { "name": "CE::Workers::Group::submit", - "label": "submit()", + "label": "submit(Work&& work): Result [future]", "file": "projects/engine/include/cheryl/core/engine/worker-pool.h", "line": 140, - "brief": "Queue an owned CPU-only callable and retain its result future; arrange owner-affine follow-up explicitly.", + "brief": "Queue an owned CPU-only callable work(). Result is the callable return type, delivered through its future; retain captures and arrange owner-affine follow-up explicitly.", "role": "call" }, { @@ -19990,7 +20014,7 @@ "Assets::Loading::Loader": [ { "name": "CE::Assets::Loading::Loader::prepare", - "label": "prepare()", + "label": "prepare(): Assets::Loading::Prepared", "file": "projects/engine/include/cheryl/core/resources/asset-management/asset-loader.h", "line": 112, "brief": "Prepare owned asset data on CPU without publishing caches; upload later on the serialized loading/graphics owner.", @@ -20000,10 +20024,10 @@ "Runtime::PlatformDispatcher::Submission": [ { "name": "CE::Runtime::PlatformDispatcher::Submission::submit", - "label": "submit()", + "label": "submit(Work&& work): Result [future]", "file": "projects/engine/include/cheryl/core/engine/platform-dispatcher.h", "line": 63, - "brief": "Post the job's owned upload data for platform work; Pool itself has no dispatch dependency.", + "brief": "Queue an owned callable work(Runtime::Context&) for later platform execution. Result is the callable return type, delivered through its future; retain captures and never wait from the owner needed to execute the work.", "role": "call" } ] @@ -20111,26 +20135,26 @@ "Memory::Objects::Pool": [ { "name": "CE::State::Singleton::get", - "label": "get()", + "label": "get(): Memory::Objects::Pool&", "file": "projects/engine/include/cheryl/templates/singleton.h", "line": 64, - "brief": "Inherited entry point: acquire the shared Pool facade before retrieving objects.", + "brief": "Borrow the default per-T pool singleton. T is the application-selected object type; the inherited accessor returns this Pool specialization.", "role": "call" }, { "name": "CE::Memory::Objects::Pool::retrieve_objects", - "label": "retrieve_objects()", + "label": "retrieve_objects(N, args...): T[] [shared elements]", "file": "projects/engine/include/cheryl/core/resources/objects/pool.h", "line": 51, - "brief": "Construct a batch now; each returned handle independently retains release state.", + "brief": "Construct N objects and return independent shared owners; T[] abridges a collection of shared-owned T objects, not a contiguous returned array.", "role": "call" }, { "name": "CE::Memory::Objects::Pool::cull", - "label": "cull()", + "label": "cull(age)", "file": "projects/engine/include/cheryl/core/resources/objects/pool.h", "line": 57, - "brief": "Optional storage maintenance: queue complete free owners old enough for release.", + "brief": "Queue complete free owners old enough for release; age is a duration in minutes and does not forcibly free checked-out ranges.", "role": "call" }, { @@ -20145,26 +20169,26 @@ "Memory::Objects::Reservation": [ { "name": "CE::Memory::Objects::Reservation::Reservation", - "label": "Reservation()", + "label": "Reservation(count, Allocator allocator)", "file": "projects/engine/include/cheryl/core/resources/objects/object-reservation.hpp", "line": 77, - "brief": "Reserve unconstructed slots and retain release state; count zero creates an empty reservation.", + "brief": "Reserve unconstructed slots with an allocator satisfying ReservationAllocator; the default allocator is Memory::Allocators::ObjectPoolAllocator.", "role": "call" }, { "name": "CE::Memory::Objects::Reservation::emplace", - "label": "emplace()", + "label": "emplace(index, args...): T [shared]", "file": "projects/engine/include/cheryl/core/resources/objects/object-reservation.hpp", "line": 102, - "brief": "Construct one unclaimed logical slot; failed construction leaves that slot available to retry.", + "brief": "Construct one unclaimed slot and return a shared owner of T. The result can outlive the reservation/pool facade; this abridged signature hides the shared-ownership wrapper.", "role": "call" }, { "name": "CE::Memory::Objects::Reservation::remaining_ranges", - "label": "remaining_ranges()", + "label": "remaining_ranges(): const Memory::Block[]& [borrowed]", "file": "projects/engine/include/cheryl/core/resources/objects/object-reservation.hpp", "line": 99, - "brief": "Borrow the remaining unclaimed partition; emplace, move and destruction can invalidate its contents.", + "brief": "Borrow the remaining unclaimed block collection. Emplace, move or destruction can change/invalidate it; [] abridges the container, not a built-in array type.", "role": "call" } ] @@ -20351,10 +20375,10 @@ "State::StateTracker": [ { "name": "CE::State::StateTracker::update", - "label": "update()", + "label": "update(T next)", "file": "projects/engine/include/cheryl/templates/state-tracker.h", "line": 20, - "brief": "Shift the current sample into previous and install the next sample; caller serializes access.", + "brief": "Shift the current sample to previous and assign next. T is the caller-selected template sample type.", "role": "call" }, { @@ -20367,43 +20391,43 @@ }, { "name": "CE::State::StateTracker::delta", - "label": "delta()", + "label": "delta(): decltype(current() - previous())", "file": "projects/engine/include/cheryl/templates/state-tracker.h", "line": 33, - "brief": "Subtract previous from current using the sampled type's units.", + "brief": "Return current minus previous. The subtraction expression determines the result type; it is not necessarily T, and its units/ownership follow that expression.", "role": "call" } ], "State::StateMachine": [ { "name": "CE::State::StateMachine::add_transition", - "label": "add_transition()", + "label": "add_transition(State from, Trigger trigger, State to, State::StateMachine::Guard guard, State::StateMachine::Action action)", "file": "projects/engine/include/cheryl/templates/state-machine.h", "line": 38, - "brief": "Append an ordered state/trigger rule with optional guard and transition action.", + "brief": "Append a rule; State and Trigger are caller-selected template types, while Guard and Action are this machine specialization's public callback aliases. Empty guard/action are permitted.", "role": "call" }, { "name": "CE::State::StateMachine::trigger", - "label": "trigger()", + "label": "trigger(const Trigger& event)", "file": "projects/engine/include/cheryl/templates/state-machine.h", "line": 47, - "brief": "Run the first eligible transition; caller serializes access and avoids recursive trigger calls.", + "brief": "Execute the first eligible transition and report whether one ran. Trigger is the caller-selected template event type.", "role": "call" }, { "name": "CE::State::StateMachine::state", - "label": "state()", + "label": "state(): const State&", "file": "projects/engine/include/cheryl/templates/state-machine.h", "line": 45, - "brief": "Borrow the current state; copy it for an independently retained sample.", + "brief": "Borrow the current caller-selected State value; State here is the template argument, not the Engine State namespace. Later transitions assign the same storage.", "role": "call" } ], "State::VersionedVariable": [ { "name": "CE::State::VersionedVariable::set", - "label": "set()", + "label": "set(T next)", "file": "projects/engine/include/cheryl/templates/versioned-variable.h", "line": 40, "brief": "Publish a changed value and advance its revision; equal values leave the revision unchanged.", @@ -20411,7 +20435,7 @@ }, { "name": "CE::State::VersionedVariable::snapshot", - "label": "snapshot()", + "label": "snapshot(): State::VersionedSnapshot", "file": "projects/engine/include/cheryl/templates/versioned-variable.h", "line": 51, "brief": "Copy the value and revision together under one storage lock.", @@ -20419,7 +20443,7 @@ }, { "name": "CE::State::VersionedVariable::wait_for_change", - "label": "wait_for_change()", + "label": "wait_for_change(since): State::VersionedSnapshot", "file": "projects/engine/include/cheryl/templates/versioned-variable.h", "line": 66, "brief": "Wait for a revision different from a retained one; updates coalesce and there is no timeout/cancellation.", @@ -20429,15 +20453,15 @@ "State::ObservedVariable": [ { "name": "CE::State::ObservedVariable::ObservedVariable", - "label": "ObservedVariable()", + "label": "ObservedVariable(T value, State::ObservedVariable::Callback[] observers)", "file": "projects/engine/include/cheryl/templates/observed-variables.h", "line": 32, - "brief": "Supply the initial value and owned fixed observer list; construction invokes no observer.", + "brief": "Store the caller-selected T value and a fixed array of Observers owned callback objects. Each Callback receives const T& synchronously; construction invokes none.", "role": "call" }, { "name": "CE::State::ObservedVariable::set", - "label": "set()", + "label": "set(T value)", "file": "projects/engine/include/cheryl/templates/observed-variables.h", "line": 40, "brief": "Publish then invoke observers on the setter's thread, even for equal values; callbacks protect their own targets.", @@ -20445,7 +20469,7 @@ }, { "name": "CE::State::ObservedVariable::snapshot", - "label": "snapshot()", + "label": "snapshot(): State::VersionedSnapshot", "file": "projects/engine/include/cheryl/templates/observed-variables.h", "line": 50, "brief": "Copy a consistent stored value/revision pair without invoking observers.", @@ -20970,7 +20994,7 @@ "Logging::Config": [ { "name": "CE::Logging::Config::for_logger", - "label": "for_logger()", + "label": "for_logger(name, Logging::Profile profile): Logging::Config", "file": "projects/engine/include/cheryl/core/logging/config.h", "line": 48, "brief": "Choose the category/profile runtime threshold defaults before its first explicit initialization.", @@ -20980,7 +21004,7 @@ "Logging::Log": [ { "name": "CE::Logging::Log::Log", - "label": "Log()", + "label": "Log(Logging::FileEventHandlers handlers, Logging::Config configuration)", "file": "projects/engine/include/cheryl/core/logging/log.h", "line": 119, "brief": "Construct an independently owned named logger with handlers and configuration; requires a live process guard.", @@ -20988,7 +21012,7 @@ }, { "name": "CE::Logging::LogHandle::info", - "label": "info()", + "label": "info(format, arguments...)", "file": "projects/engine/include/cheryl/core/logging/log.h", "line": 92, "brief": "Inherited writer: format and submit an informational record only when compiled and admitted by runtime filtering.", @@ -20996,17 +21020,17 @@ }, { "name": "CE::Logging::LogHandle::close", - "label": "close()", + "label": "close(timeout)", "file": "projects/engine/include/cheryl/core/logging/log.h", "line": 73, - "brief": "Inherited lifecycle operation: close after producer quiescence; a timed close may be incomplete.", + "brief": "Drain accepted records and close on the application owner; timeout is a milliseconds duration, and zero waits without a timeout. Keep callers stopped through close.", "role": "call" } ], "Logging::Logger": [ { "name": "CE::Logging::Logger::initialize", - "label": "initialize()", + "label": "initialize(Logging::FileEventHandlers handlers, Logging::Config config): Logging::Log&", "file": "projects/engine/include/cheryl/core/logging/logger.h", "line": 51, "brief": "Explicitly configure the shared named category before lazy access or writers; repeated explicit initialization rejects.", @@ -21014,7 +21038,7 @@ }, { "name": "CE::Logging::Logger::info", - "label": "info()", + "label": "info(fmt, args...)", "file": "projects/engine/include/cheryl/core/logging/logger.h", "line": 121, "brief": "Emit an informational record through the shared named category.", @@ -21022,7 +21046,7 @@ }, { "name": "CE::Logging::Logger::warn", - "label": "warn()", + "label": "warn(fmt, args...)", "file": "projects/engine/include/cheryl/core/logging/logger.h", "line": 129, "brief": "Emit a warning through the shared named category; keep the original operation error authoritative.", @@ -21032,7 +21056,7 @@ "Logging::LogHandle": [ { "name": "CE::Logging::LogHandle::shared_queue_stats", - "label": "shared_queue_stats()", + "label": "shared_queue_stats(): Logging::QueueStats", "file": "projects/engine/include/cheryl/core/logging/log.h", "line": 67, "brief": "Copy observations of the shared Cheryl queue; counts include flush requests and are not a completion barrier.", @@ -21040,7 +21064,7 @@ }, { "name": "CE::Logging::LogHandle::backend_stats", - "label": "backend_stats()", + "label": "backend_stats(): Logging::BackendStats", "file": "projects/engine/include/cheryl/core/logging/log.h", "line": 68, "brief": "Observe backend health and destination activity for this logger.", @@ -21459,7 +21483,7 @@ "Runtime::GameRuntime": [ { "name": "CE::Runtime::GameRuntime::diagnostics", - "label": "diagnostics()", + "label": "diagnostics(): Runtime::RuntimeStats", "file": "projects/engine/include/cheryl/core/game-framework/game-runtime.h", "line": 111, "brief": "Copy unstarted or completed-session observations after synchronization with run(); live sampling rejects.", @@ -21469,7 +21493,7 @@ "Workers::Group": [ { "name": "CE::Workers::Group::status", - "label": "status()", + "label": "status(): Workers::GroupStatus", "file": "projects/engine/include/cheryl/core/engine/worker-pool.h", "line": 168, "brief": "Copy workload activity and failure counters; a snapshot is not a completion barrier.", @@ -21477,7 +21501,7 @@ }, { "name": "CE::Workers::Group::policy", - "label": "policy()", + "label": "policy(): Workers::GroupPolicy", "file": "projects/engine/include/cheryl/core/engine/worker-pool.h", "line": 169, "brief": "Copy the requested/effective policy and preferred fallback recorded at creation.", @@ -21487,7 +21511,7 @@ "Runtime::PlatformDispatcher": [ { "name": "CE::Runtime::PlatformDispatcher::diagnostics", - "label": "diagnostics()", + "label": "diagnostics(): Diagnostics::DispatchStats", "file": "projects/engine/include/cheryl/core/engine/platform-dispatcher.h", "line": 103, "brief": "Observe pending, running and cumulative activity; pending alone excludes detached work.", @@ -21497,7 +21521,7 @@ "Events::EventBus": [ { "name": "CE::Events::EventBus::diagnostics", - "label": "diagnostics()", + "label": "diagnostics(): Events::EventStats", "file": "projects/engine/include/cheryl/core/subsystems/event-bus.h", "line": 253, "brief": "Copy registration and delivery counters; closure and counters do not drain callbacks.", @@ -21507,7 +21531,7 @@ "Logging::Logger": [ { "name": "CE::Logging::Logger::info", - "label": "info()", + "label": "info(fmt, args...)", "file": "projects/engine/include/cheryl/core/logging/logger.h", "line": 121, "brief": "Format the application's copied observations outside owner locks/native callbacks; filtering may suppress the report.", @@ -21692,15 +21716,15 @@ "Files::FileIndex": [ { "name": "CE::Files::FileIndex::FileIndex", - "label": "FileIndex()", + "label": "FileIndex(const Files::Path& root_path)", "file": "projects/engine/include/cheryl/core/resources/fileio/file-mgr.h", "line": 23, - "brief": "Scan an explicitly supplied root; construct a new index to rescan previously visited directories.", + "brief": "Scan an explicit root. Files::Path is the public alias of the filesystem path type spelled in the constructor declaration; construct a new index to rescan visited roots.", "role": "call" }, { "name": "CE::Files::FileIndex::get_files_of_type", - "label": "get_files_of_type()", + "label": "get_files_of_type(Files::Extension extension): const Files::Path[]& [borrowed]", "file": "projects/engine/include/cheryl/core/resources/fileio/file-mgr.h", "line": 27, "brief": "Borrow an extension bucket using a leading dot; a missing extension inserts an empty bucket.", @@ -21708,10 +21732,10 @@ }, { "name": "CE::Files::FileIndex::search_directory", - "label": "search_directory()", + "label": "search_directory(const Files::Path& directory)", "file": "projects/engine/include/cheryl/core/resources/fileio/file-mgr.h", "line": 25, - "brief": "Add an unvisited directory tree; discovery can block and may leave a partial index on failure.", + "brief": "Add an unvisited tree; Files::Path is the public alias of the filesystem path type spelled in the declaration. Scanning can block and fail after partial indexing.", "role": "call" } ], @@ -21726,35 +21750,35 @@ }, { "name": "CE::Time::DeltaTime::elapsed", - "label": "elapsed()", + "label": "elapsed(): Time::DeltaTime::Duration", "file": "projects/engine/include/cheryl/templates/delta.h", "line": 41, - "brief": "Observe the native clock duration without starting a new interval.", + "brief": "Observe the selected clock's native Duration through the public alias; this call does not begin a new interval.", "role": "call" } ], "Math::BitArray": [ { "name": "CE::Math::BitArray::operator[]", - "label": "operator[]()", + "label": "operator[](index): Math::BitArray::Reference", "file": "projects/engine/include/cheryl/math/binary.h", "line": 79, - "brief": "Read or write one valid bit index; the mutable proxy borrows the array and access requires caller synchronization.", + "brief": "The shown mutable overload returns the public write-through Reference alias, which borrows this array; the const overload returns a scalar bit value. Use a valid index and caller synchronization.", "role": "call" }, { "name": "CE::Math::BitArray::words", - "label": "words()", + "label": "words(): Math::BitArray::word_type[] [borrowed]", "file": "projects/engine/include/cheryl/math/binary.h", "line": 82, - "brief": "Borrow native packed words; their representation is not portable serialization.", + "brief": "The shown mutable overload returns a borrowed view of packed word_type elements; the const overload borrows const words. The public alias hides the implementation-selected native integer type, and [] abridges the span wrapper.", "role": "call" } ], "Rendering::Geometry::Anchor": [ { "name": "CE::Rendering::Geometry::Anchor::MakeQuad", - "label": "MakeQuad()", + "label": "MakeQuad(Rendering::Geometry::Pivot pivot, texture_width, texture_height, width, height, x0, y0): Rendering::Geometry::Quad", "file": "projects/engine/include/cheryl/math/anchor.h", "line": 46, "brief": "Create six CPU vertices from a normalized pivot and pixel rectangle; require finite valid pivot and nonzero texture size.", @@ -21762,13 +21786,18 @@ }, { "name": "CE::Rendering::Geometry::Anchor::MakeQuadStrip", - "label": "MakeQuadStrip()", + "label": "MakeQuadStrip(Rendering::Geometry::Pivot pivot, texture_width, texture_height, width, height, x0, y0): Rendering::Geometry::QuadTriangleStrip", "file": "projects/engine/include/cheryl/math/anchor.h", "line": 55, "brief": "Create a four-vertex CPU quad strip using the same coordinate and dimension requirements.", "role": "call" } ] + }, + "operation_labels": { + "Memory::adjust_length()": "Memory::adjust_length(length, Memory::FitPolicy fit, gb, gf)", + "Text::Numbers::parse_integers()": "Text::Numbers::parse_integers(str): Text::Numbers::NumberVariant", + "Text::Numbers::parse_floats()": "Text::Numbers::parse_floats(str)" } }, { diff --git a/docs/site/engine-systems/diagrams/diagnostics-application.mmd b/docs/site/engine-systems/diagrams/diagnostics-application.mmd index 4d5b2521..cd33deb6 100644 --- a/docs/site/engine-systems/diagrams/diagnostics-application.mmd +++ b/docs/site/engine-systems/diagrams/diagnostics-application.mmd @@ -15,16 +15,16 @@ flowchart TB accTitle: Diagnostics · application observations accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
report"]):::app - k1["Runtime::
GameRuntime


diagnostics()"]:::neighbor + k1["Runtime::
GameRuntime


diagnostics(): Runtime::RuntimeStats"]:::neighbor k2["Runtime::
RuntimeStats"]:::focal k3(["Application
live observations"]):::app - k4["Workers::
Group


status()
policy()"]:::neighbor + k4["Workers::
Group


status(): Workers::GroupStatus

policy(): Workers::GroupPolicy"]:::neighbor k5["Workers::
GroupStatus"]:::focal - k6["Runtime::
PlatformDispatcher


diagnostics()"]:::neighbor + k6["Runtime::
PlatformDispatcher


diagnostics(): Diagnostics::DispatchStats"]:::neighbor k7["Diagnostics::
DispatchStats"]:::focal - k8["Events::
EventBus


diagnostics()"]:::neighbor + k8["Events::
EventBus


diagnostics(): Events::EventStats"]:::neighbor k9["Events::
EventStats"]:::focal - k10["Logging::
Logger


info()"]:::neighbor + k10["Logging::
Logger


info(fmt, args...)"]:::neighbor k11["Workers::
GroupPolicy"]:::focal k0 --> k1 k1 --> k2 diff --git a/docs/site/engine-systems/diagrams/dispatch-application.mmd b/docs/site/engine-systems/diagrams/dispatch-application.mmd index 499dfe2a..2b18bedb 100644 --- a/docs/site/engine-systems/diagrams/dispatch-application.mmd +++ b/docs/site/engine-systems/diagrams/dispatch-application.mmd @@ -15,16 +15,16 @@ flowchart LR accTitle: Dispatch · application owner requests accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Runtime::
Context


platform_dispatcher()
resources()"]:::neighbor - k2["Runtime::
PlatformDispatcher


submission()"]:::focal - k3["Runtime::PlatformDispatcher::
Submission


submit()"]:::focal + k1["Runtime::
Context


platform_dispatcher(): Runtime::PlatformDispatcher&

resources(): Rendering::Resources::Provider&"]:::neighbor + k2["Runtime::
PlatformDispatcher


submission(): Runtime::PlatformDispatcher::Submission"]:::focal + k3["Runtime::PlatformDispatcher::
Submission


submit(Work&& work): Result [future]"]:::focal k4(["Application
producer"]):::app k5(["Application
platform-owner work"]):::app - k6["Rendering::Resources::
Provider


create_image()
upload_geometry()"]:::neighbor + k6["Rendering::Resources::
Provider


create_image(const Assets::Images::Decoded& image):
Rendering::Resources::Image [shared]

upload_geometry(
const Rendering::Geometry::Vertex2D[] vertices
[borrowed],
Rendering::Geometry::Topology topology):
Rendering::Geometry::Buffer2D [shared]"]:::neighbor k7(["Application
composition"]):::app - k8["Runtime::
GameRuntime


simulation_dispatcher()"]:::neighbor - k9["Runtime::
SimulationDispatcher


submission()"]:::focal - k10["Runtime::SimulationDispatcher::
Submission


submit()"]:::focal + k8["Runtime::
GameRuntime


simulation_dispatcher(): Runtime::SimulationDispatcher&"]:::neighbor + k9["Runtime::
SimulationDispatcher


submission(): Runtime::SimulationDispatcher::Submission"]:::focal + k10["Runtime::SimulationDispatcher::
Submission


submit(Work&& work): Result [future]"]:::focal k11(["Application
simulation-owner work"]):::app k12["Game::
TickContext"]:::neighbor k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/display-application.mmd b/docs/site/engine-systems/diagrams/display-application.mmd index c9a8a220..f9c071dd 100644 --- a/docs/site/engine-systems/diagrams/display-application.mmd +++ b/docs/site/engine-systems/diagrams/display-application.mmd @@ -15,18 +15,18 @@ flowchart LR accTitle: Display · application access accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Runtime::
Context


display()
window()
platform_dispatcher()"]:::neighbor + k1["Runtime::
Context


display(): Platform::DisplaySystem&

window(): Platform::Window&

platform_dispatcher(): Runtime::PlatformDispatcher&"]:::neighbor k2(["Application
platform-owner work"]):::app - k3["Runtime::PlatformDispatcher::
Submission


submit()"]:::neighbor - k4["Platform::
Window


resize()
set_mode()
hide_cursor()"]:::focal - k5["Platform::
DisplaySystem


monitors()
primary_monitor()
content_scale()"]:::focal + k3["Runtime::PlatformDispatcher::
Submission


submit(Work&& work): Result [future]"]:::neighbor + k4["Platform::
Window


resize(width, height)

set_mode(Platform::WindowMode mode)

hide_cursor(hide)"]:::focal + k5["Platform::
DisplaySystem


monitors(): const Platform::Monitor[]&

primary_monitor(): const Platform::Monitor&

content_scale(const Platform::Monitor& monitor)"]:::focal k6["Platform::
Monitor"]:::focal k7["Platform::
Dimensions"]:::focal k8["Platform::
WindowMode"]:::focal k9(["Application
update()"]):::app k10["Game::
TickContext"]:::neighbor k11["Platform::
FramebufferSize"]:::focal - k12["Rendering::
Camera2D


set_framebuffer_size()"]:::neighbor + k12["Rendering::
Camera2D


set_framebuffer_size(Platform::FramebufferSize size)"]:::neighbor k0 --> k1 k2 --> k3 k2 --> k4 diff --git a/docs/site/engine-systems/diagrams/events-application.mmd b/docs/site/engine-systems/diagrams/events-application.mmd index 7ad31bc5..c145fb60 100644 --- a/docs/site/engine-systems/diagrams/events-application.mmd +++ b/docs/site/engine-systems/diagrams/events-application.mmd @@ -16,13 +16,13 @@ flowchart LR accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
producer"]):::app k1["Events::
EventChannel"]:::focal - k2["Events::EventBus::
Publisher


notify()"]:::focal + k2["Events::EventBus::
Publisher


notify(
const Events::EventChannel<Payload>& event,
Value&& payload,
const Events::EventBus::ErrorHandler& errors)"]:::focal k3(["Application
listener setup"]):::app - k4["Runtime::
Context


events()"]:::neighbor - k5["Events::
EventBus


register_listener()
publisher()
unregister_and_wait()"]:::focal + k4["Runtime::
Context


events(): Events::EventBus&"]:::neighbor + k5["Events::
EventBus


register_listener(
const Events::EventChannel<Payload>& event,
Events::EventChannel<Payload>::Callback callback,
Events::EventBus::Delivery delivery,
Events::EventBus::ErrorHandler errors):
Events::EventBus::Registration

publisher(): Events::EventBus::Publisher

unregister_and_wait(
const Events::EventBus::Registration& registration)"]:::focal k6["Events::EventBus::
Registration


disconnect()"]:::focal k7(["Application
callback"]):::app - k8("Events::
simulation_delivery()"):::focal + k8("Events::simulation_delivery(
Runtime::SimulationDispatcher::Submission endpoint):
Events::EventBus::Delivery"):::focal k9["Runtime::SimulationDispatcher::
Submission"]:::neighbor k10["Events::EventBus::
Delivery"]:::focal k11["Events::EventBus::
ErrorHandler"]:::focal diff --git a/docs/site/engine-systems/diagrams/gui-application.mmd b/docs/site/engine-systems/diagrams/gui-application.mmd index 73f4e459..86381324 100644 --- a/docs/site/engine-systems/diagrams/gui-application.mmd +++ b/docs/site/engine-systems/diagrams/gui-application.mmd @@ -15,18 +15,18 @@ flowchart TB accTitle: GUI · application-owned sessions and publication accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application-owned UI"]):::app - k1["UI::TGUI::
Session


gui()
handle_input()
request_keyboard_focus()
record()"]:::focal - k2["UI::RmlUi::
Session


context()
handle_input()
request_keyboard_focus()
record()"]:::focal + k1["UI::TGUI::
Session


gui(): tgui::BackendGui&

handle_input(
const Input::Events::Record[] records [borrowed],
pointer_selected)

request_keyboard_focus(
Input::Routing::KeyboardPolicy routing)

record(): UI::TGUI::RecordedScene"]:::focal + k2["UI::RmlUi::
Session


context(): Rml::Context&

handle_input(
const Input::Events::Record[] records [borrowed],
pointer_selected)

request_keyboard_focus(
Input::Routing::KeyboardPolicy routing)

record(): UI::RmlUi::RecordedScene"]:::focal k3["Input::Events::
Record"]:::neighbor k4["Input::Routing::
KeyboardPolicy"]:::neighbor k5["Text::
Font"]:::neighbor k6["UI::TGUI::
RecordedScene"]:::focal k7["UI::RmlUi::
RecordedScene"]:::focal k8(["Application
init_platform()"]):::app - k9["UI::TGUI::
SceneUploader


upload()
submit()"]:::focal - k10["UI::RmlUi::
SceneUploader


upload()
submit()"]:::focal - k11["UI::TGUI::
Scene


write()"]:::focal - k12["UI::RmlUi::
Scene


write()"]:::focal + k9["UI::TGUI::
SceneUploader


upload(
Rendering::Resources::Provider& provider,
const UI::TGUI::RecordedScene& recording):
UI::TGUI::Scene

submit(
const Runtime::PlatformDispatcher::Submission&
platform,
UI::TGUI::RecordedScene recording):
UI::TGUI::Scene [future]"]:::focal + k10["UI::RmlUi::
SceneUploader


upload(
Rendering::Resources::Provider& provider,
const UI::RmlUi::RecordedScene& recording):
UI::RmlUi::Scene

submit(
const Runtime::PlatformDispatcher::Submission&
platform,
UI::RmlUi::RecordedScene recording):
UI::RmlUi::Scene [future]"]:::focal + k11["UI::TGUI::
Scene


write(Rendering::FrameWriter& frame)"]:::focal + k12["UI::RmlUi::
Scene


write(Rendering::FrameWriter& frame)"]:::focal k13(["Application
prepare_render_frame()"]):::app k14["Rendering::
FrameWriter"]:::neighbor k0 -.-> k1 diff --git a/docs/site/engine-systems/diagrams/input-application.mmd b/docs/site/engine-systems/diagrams/input-application.mmd index f97b5f82..dff7e0da 100644 --- a/docs/site/engine-systems/diagrams/input-application.mmd +++ b/docs/site/engine-systems/diagrams/input-application.mmd @@ -15,19 +15,19 @@ flowchart LR accTitle: Input · application actions and controllers accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Runtime::
Context


input()"]:::neighbor - k2["Input::
System


bindings()
action_contexts()
keyboard_id()"]:::focal - k3["Input::Actions::
Bindings


bind_button()
bind_axis()
unbind()"]:::focal + k1["Runtime::
Context


input(): Input::System&"]:::neighbor + k2["Input::
System


bindings(): Input::Actions::Bindings&

action_contexts(): Input::Actions::ContextSubmission

keyboard_id(): Input::DeviceId"]:::focal + k3["Input::Actions::
Bindings


bind_button(
Input::Control control,
Input::Actions::Id action):
Input::Actions::BindingId

bind_axis(
Input::Control axis,
Input::Actions::Id action,
Input::Actions::AxisOptions options):
Input::Actions::BindingId

unbind(Input::Actions::BindingId binding)"]:::focal k4["Input::
Control"]:::focal k5["Input::
KeyboardKey"]:::focal k6(["Application-owned controller"]):::app k7["Input::Actions::
ContextDefinition"]:::focal - k8["Input::Actions::
ContextSubmission


attach(Input::Actions::ContextDefinition)"]:::focal - k9["Input::Actions::
Context


id()
reset()"]:::focal + k8["Input::Actions::
ContextSubmission


attach(Input::Actions::ContextDefinition definition):
Input::Actions::Context"]:::focal + k9["Input::Actions::
Context


id(): Input::Actions::ContextId

reset()"]:::focal k10(["Application
update()"]):::app k11["Game::
TickContext"]:::neighbor - k12["Input::
Tick


actions(Input::Actions::Context)
button()
axis()"]:::focal - k13["Input::Actions::
View


button()
axis()"]:::focal + k12["Input::
Tick


actions(const Input::Actions::Context& context):
Input::Actions::View [borrowed]

button(Input::Actions::Id action):
Input::Actions::ButtonTickState

axis(Input::Actions::Id action):
Input::Actions::AxisTickState"]:::focal + k13["Input::Actions::
View


button(Input::Actions::Id action):
Input::Actions::ButtonTickState

axis(Input::Actions::Id action):
Input::Actions::AxisTickState"]:::focal k2 --> k3 k3 --> k4 k4 --> k5 diff --git a/docs/site/engine-systems/diagrams/logging-application.mmd b/docs/site/engine-systems/diagrams/logging-application.mmd index 8fb33ff0..3cc856a0 100644 --- a/docs/site/engine-systems/diagrams/logging-application.mmd +++ b/docs/site/engine-systems/diagrams/logging-application.mmd @@ -16,16 +16,16 @@ flowchart TB accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app k1["Logging::
ProcessLifetime


ProcessLifetime()"]:::focal - k2["Logging::
Config


for_logger()"]:::focal - k3["Logging::
Log


Log()
info()
close()"]:::focal + k2["Logging::
Config


for_logger(name, Logging::Profile profile):
Logging::Config"]:::focal + k3["Logging::
Log


Log(
Logging::FileEventHandlers handlers,
Logging::Config configuration)

info(format, arguments...)

close(timeout)"]:::focal k4["Logging::
Profile"]:::focal k5["Logging::
Level"]:::focal k6["Logging::
OverflowPolicy"]:::focal k7["Logging::
FileEventHandlers"]:::focal k8(["Application
producer"]):::app - k9["Logging::
Logger


initialize()
info()
warn()"]:::focal + k9["Logging::
Logger


initialize(
Logging::FileEventHandlers handlers,
Logging::Config config):
Logging::Log<name>&

info(fmt, args...)

warn(fmt, args...)"]:::focal k10(["Application
report"]):::app - k11["Logging::
LogHandle


shared_queue_stats()
backend_stats()
report_diagnostics()"]:::focal + k11["Logging::
LogHandle


shared_queue_stats(): Logging::QueueStats

backend_stats(): Logging::BackendStats

report_diagnostics()"]:::focal k12["Logging::
QueueStats"]:::neighbor k13(["Application
cleanup / quiesce()"]):::app k14["Logging::
BackendStats"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/memory-application.mmd b/docs/site/engine-systems/diagrams/memory-application.mmd index a82fe72d..073819be 100644 --- a/docs/site/engine-systems/diagrams/memory-application.mmd +++ b/docs/site/engine-systems/diagrams/memory-application.mmd @@ -15,8 +15,8 @@ flowchart TB accTitle: Memory · application object storage accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
storage owner"]):::app - k1["Memory::Objects::
Pool


get()
retrieve_objects()
cull()
release_culled()"]:::focal - k2["Memory::Objects::
Reservation


Reservation()
emplace()
remaining_ranges()"]:::focal + k1["Memory::Objects::
Pool


get(): Memory::Objects::Pool<T>&

retrieve_objects(N, args...): T[] [shared elements]

cull(age)

release_culled()"]:::focal + k2["Memory::Objects::
Reservation


Reservation(count, Allocator allocator)

emplace(index, args...): T [shared]

remaining_ranges(): const Memory::Block<T>[]& [borrowed]"]:::focal k3(["Application
object type"]):::app k4(["Application-owned game objects"]):::app k5(["Application
cleanup / quiesce()"]):::app diff --git a/docs/site/engine-systems/diagrams/rendering-application.mmd b/docs/site/engine-systems/diagrams/rendering-application.mmd index 0bd6f4a1..421f0b20 100644 --- a/docs/site/engine-systems/diagrams/rendering-application.mmd +++ b/docs/site/engine-systems/diagrams/rendering-application.mmd @@ -15,13 +15,13 @@ flowchart LR accTitle: Rendering · application frame preparation accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
update()"]):::app - k1["Rendering::
Camera2D


set_framebuffer_size()
set_view_matrix()
projection_matrix()
view_matrix()"]:::focal + k1["Rendering::
Camera2D


set_framebuffer_size(Platform::FramebufferSize size)

set_view_matrix(const glm::mat4& view)

projection_matrix(): const glm::mat4&

view_matrix(): const glm::mat4&"]:::focal k2(["Application
prepare_render_frame()"]):::app - k3["Rendering::
FrameWriter


begin_pass()"]:::focal - k4["Rendering::
PassWriter


add()
semantics()
parameters()
constraints()"]:::focal + k3["Rendering::
FrameWriter


begin_pass(
const glm::mat4& projection,
const glm::mat4& view,
Rendering::Pipelines::PassConstraints2D constraints,
Rendering::Parameters::Set parameters):
Rendering::PassWriter [borrowed]"]:::focal + k4["Rendering::
PassWriter


add(Rendering::DrawPacket2D draw)

semantics(): Rendering::Parameters::PassSemantics

parameters(): const Rendering::Parameters::Set&

constraints():
const Rendering::Pipelines::PassConstraints2D&"]:::focal k5["Rendering::Submission::
Context2D"]:::focal k6["Rendering::
DrawStyle2D"]:::focal - k7("Rendering::Submission::
resolve_sprite()"):::focal + k7("Rendering::Submission::resolve_sprite(
const Assets::Sprites::Asset& sprite,
Assets::Grids::CellIndex cell,
const Rendering::DrawStyle2D& style,
const Rendering::Submission::Context2D& context):
Rendering::DrawPacket2D"):::focal k8["Assets::Sprites::
Asset"]:::neighbor k9["Rendering::
DrawPacket2D"]:::focal k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/resources-application.mmd b/docs/site/engine-systems/diagrams/resources-application.mmd index cd642b28..129090d5 100644 --- a/docs/site/engine-systems/diagrams/resources-application.mmd +++ b/docs/site/engine-systems/diagrams/resources-application.mmd @@ -15,11 +15,11 @@ flowchart TB accTitle: Resources · application uploads and retained draws accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
loading work"]):::app - k1("Assets::Images::
decode()"):::neighbor + k1("Assets::Images::decode(file): Assets::Images::Decoded"):::neighbor k2["Assets::Images::
Decoded"]:::focal k3(["Application
init_platform()"]):::app - k4["Runtime::
Context


resources()"]:::neighbor - k5["Rendering::Resources::
Provider


create_image()
upload_geometry()
graphics_domain()"]:::focal + k4["Runtime::
Context


resources(): Rendering::Resources::Provider&"]:::neighbor + k5["Rendering::Resources::
Provider


create_image(const Assets::Images::Decoded& image):
Rendering::Resources::Image [shared]

upload_geometry(
const Rendering::Geometry::Vertex2D[] vertices
[borrowed],
Rendering::Geometry::Topology topology):
Rendering::Geometry::Buffer2D [shared]

graphics_domain(): Rendering::GraphicsDomain"]:::focal k6(["Application
platform-owner work"]):::app k7["Rendering::Resources::
Image"]:::focal k8["Rendering::Geometry::
Buffer2D"]:::focal @@ -28,7 +28,7 @@ flowchart TB k11(["Application
prepare_render_frame()"]):::app k12["Rendering::
DrawPacket2D"]:::neighbor k13(["Application
report"]):::app - k14["Rendering::
GraphicsDomain


snapshot()"]:::focal + k14["Rendering::
GraphicsDomain


snapshot(): Rendering::GraphicsDomainSnapshot"]:::focal k0 --> k1 k1 --> k2 k3 --> k4 diff --git a/docs/site/engine-systems/diagrams/runtime-application.mmd b/docs/site/engine-systems/diagrams/runtime-application.mmd index 540cf849..da995434 100644 --- a/docs/site/engine-systems/diagrams/runtime-application.mmd +++ b/docs/site/engine-systems/diagrams/runtime-application.mmd @@ -15,19 +15,19 @@ flowchart TB accTitle: Runtime · application composition accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app - k1("Application::GLFWOpenGL::
make_context()"):::focal + k1("Application::GLFWOpenGL::make_context(
const Application::GLFWOpenGL::Options& config):
Runtime::Context [unique]"):::focal k2["Runtime::
Context"]:::focal - k3["Runtime::
GameRuntime


run()
shutdown()
lifecycle()
set_shutdown_policy()"]:::focal + k3["Runtime::
GameRuntime


run()

shutdown(): Runtime::ShutdownEndpoint

lifecycle(): Runtime::RuntimeLifecycle

set_shutdown_policy(
Runtime::ShutdownPolicyCallback callback,
Runtime::ShutdownPolicyOptions options,
Events::EventBus::Delivery delivery):
Runtime::ShutdownPolicy"]:::focal k4(["Application-owned game objects"]):::app - k5["Game::
AbstractGame


Implement hooks
init_platform()
init_simulation()
update(tick)
prepare_render_frame(frame)"]:::focal + k5["Game::
AbstractGame


Implement hooks
init_platform()

init_simulation()

update(const Game::TickContext& tick)

prepare_render_frame(Rendering::FrameWriter& frame)"]:::focal k6(["Application
update()"]):::app k7["Game::
TickContext


request_stop()"]:::focal k8(["Application
prepare_render_frame()"]):::app - k9["Rendering::
FrameWriter


begin_pass()"]:::neighbor + k9["Rendering::
FrameWriter


begin_pass(
const glm::mat4& projection,
const glm::mat4& view,
Rendering::Pipelines::PassConstraints2D constraints,
Rendering::Parameters::Set parameters):
Rendering::PassWriter [borrowed]"]:::neighbor k10["Runtime::
ShutdownPolicy


reset()"]:::focal k11(["Application
producer"]):::app - k12["Runtime::
ShutdownEndpoint


request()"]:::focal - k13["Runtime::
RuntimeLifecycle


snapshot()"]:::focal + k12["Runtime::
ShutdownEndpoint


request(Runtime::ShutdownReason reason):
Runtime::ShutdownTicket"]:::focal + k13["Runtime::
RuntimeLifecycle


snapshot(): Runtime::RuntimeLifecycleSnapshot"]:::focal k14(["Application
report"]):::app k0 --> k1 k1 --> k2 diff --git a/docs/site/engine-systems/diagrams/state-application.mmd b/docs/site/engine-systems/diagrams/state-application.mmd index 150a53a0..c4cf12af 100644 --- a/docs/site/engine-systems/diagrams/state-application.mmd +++ b/docs/site/engine-systems/diagrams/state-application.mmd @@ -15,11 +15,11 @@ flowchart TB accTitle: State · application state choices accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
state model"]):::app - k1["State::
StateTracker


update()
changed()
delta()"]:::focal - k2["State::
StateMachine


add_transition()
trigger()
state()"]:::focal + k1["State::
StateTracker


update(T next)

changed()

delta(): decltype(current() - previous())"]:::focal + k2["State::
StateMachine


add_transition(
State from,
Trigger trigger,
State to,
State::StateMachine<State, Trigger>::Guard guard,
State::StateMachine<State, Trigger>::Action action)

trigger(const Trigger& event)

state(): const State&"]:::focal k3(["Application
state producer"]):::app - k4["State::
VersionedVariable


set()
snapshot()
wait_for_change()"]:::focal - k5["State::
ObservedVariable


ObservedVariable()
set()
snapshot()"]:::focal + k4["State::
VersionedVariable


set(T next)

snapshot(): State::VersionedSnapshot<T>

wait_for_change(since): State::VersionedSnapshot<T>"]:::focal + k5["State::
ObservedVariable


ObservedVariable(
T value,
State::ObservedVariable<T, Observers>::Callback[]
observers)

set(T value)

snapshot(): State::VersionedSnapshot<T>"]:::focal k6["State::
VersionedSnapshot"]:::focal k7(["Application
snapshot reader"]):::app k8(["Application
transition / observer hooks"]):::app diff --git a/docs/site/engine-systems/diagrams/text-application.mmd b/docs/site/engine-systems/diagrams/text-application.mmd index fda75d4e..cec2869e 100644 --- a/docs/site/engine-systems/diagrams/text-application.mmd +++ b/docs/site/engine-systems/diagrams/text-application.mmd @@ -15,23 +15,23 @@ flowchart TB accTitle: Text · application font and label choices accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Runtime::
Context


fonts()"]:::neighbor - k2["Text::
FontService


configure()
default_font()
get()"]:::focal + k1["Runtime::
Context


fonts(): Text::FontService&"]:::neighbor + k2["Text::
FontService


configure(const Text::FontConfiguration& configuration)

default_font(): Text::Font

get(reference): Text::Font"]:::focal k3(["Application-owned UI"]):::app k4["Text::
Font


reference()"]:::focal k5(["Application
loading work"]):::app - k6("Text::
layout_text()"):::focal - k7["Text::
ShapedText


glyphs()
lines()"]:::focal - k8("Text::Rendering::
prepare()"):::focal + k6("Text::layout_text(
const Text::Font& font,
text,
const Text::LayoutOptions& options):
Text::ShapedText"):::focal + k7["Text::
ShapedText


glyphs(): const Text::ShapedGlyph[] [borrowed]

lines(): const Text::TextLine[] [borrowed]"]:::focal + k8("Text::Rendering::prepare(
Text::ShapedText layout,
Text::Rendering::RasterOptions options):
Text::Rendering::Prepared"):::focal k9["Text::Rendering::
Prepared"]:::focal - k10("Text::Rendering::
upload()"):::focal + k10("Text::Rendering::upload(
Text::Rendering::Prepared prepared,
Rendering::Resources::Provider& provider,
const Rendering::Resources::Material material
[optional shared]):
Text::Rendering::Generation"):::focal k11["Rendering::Resources::
Provider"]:::neighbor k12["Text::Rendering::
Generation"]:::focal k13(["Application
update()"]):::app - k14["Text::Rendering::
Updater


request()
update()
current()"]:::focal + k14["Text::Rendering::
Updater


request(Text::Rendering::Request next)

update()

current(): const Text::Rendering::Generation [shared]"]:::focal k15["Text::Rendering::
Request"]:::focal k16(["Application
prepare_render_frame()"]):::app - k17("Rendering::Submission::
resolve_text()"):::neighbor + k17("Rendering::Submission::resolve_text(
const Text::Rendering::Generation& text,
const Rendering::DrawStyle2D& style,
const Rendering::Submission::Context2D& context):
Rendering::DrawPacket2D[]"):::neighbor k18["Rendering::
DrawPacket2D"]:::neighbor k0 --> k1 k1 --> k2 diff --git a/docs/site/engine-systems/diagrams/utilities-application.mmd b/docs/site/engine-systems/diagrams/utilities-application.mmd index fd5fa091..dc46b776 100644 --- a/docs/site/engine-systems/diagrams/utilities-application.mmd +++ b/docs/site/engine-systems/diagrams/utilities-application.mmd @@ -15,19 +15,19 @@ flowchart TB accTitle: Utilities · application-local helpers accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
loading tool"]):::app - k1["Files::
FileIndex


FileIndex()
get_files_of_type()
search_directory()"]:::focal - k2["Time::
DeltaTime


operator()()
elapsed()"]:::focal + k1["Files::
FileIndex


FileIndex(const Files::Path& root_path)

get_files_of_type(Files::Extension extension):
const Files::Path[]& [borrowed]

search_directory(const Files::Path& directory)"]:::focal + k2["Time::
DeltaTime


operator()()

elapsed(): Time::DeltaTime<ClockType>::Duration"]:::focal k3(["Application
bit bookkeeping"]):::app - k4["Math::
BitArray


operator[]()
words()"]:::focal + k4["Math::
BitArray


operator[](index): Math::BitArray<Bits>::Reference

words(): Math::BitArray<Bits>::word_type[] [borrowed]"]:::focal k5(["Application
geometry preparation"]):::app - k6["Rendering::
Geometry::Anchor


MakeQuad()
MakeQuadStrip()"]:::focal + k6["Rendering::
Geometry::Anchor


MakeQuad(
Rendering::Geometry::Pivot pivot,
texture_width,
texture_height,
width,
height,
x0,
y0):
Rendering::Geometry::Quad

MakeQuadStrip(
Rendering::Geometry::Pivot pivot,
texture_width,
texture_height,
width,
height,
x0,
y0):
Rendering::Geometry::QuadTriangleStrip"]:::focal k7["Rendering::Geometry::
Pivot"]:::focal k8(["Application
storage owner"]):::app - k9("Memory::
adjust_length()"):::focal + k9("Memory::adjust_length(
length,
Memory::FitPolicy fit,
gb,
gf)"):::focal k10["Memory::
FitPolicy"]:::focal k11(["Application
configuration parsing"]):::app - k12("Text::
Numbers::parse_integers()"):::focal - k13("Text::
Numbers::parse_floats()"):::focal + k12("Text::Numbers::parse_integers<is_unsigned>(str):
Text::Numbers::NumberVariant"):::focal + k13("Text::Numbers::parse_floats(str)"):::focal k0 --> k1 k0 --> k2 k3 --> k4 diff --git a/docs/site/engine-systems/diagrams/workers-application.mmd b/docs/site/engine-systems/diagrams/workers-application.mmd index b060a9f4..3c68e11a 100644 --- a/docs/site/engine-systems/diagrams/workers-application.mmd +++ b/docs/site/engine-systems/diagrams/workers-application.mmd @@ -16,14 +16,14 @@ flowchart LR accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app k1["Runtime::
ExecutionOptions"]:::neighbor - k2["Workers::
Pool


make_group()
shutdown()"]:::focal - k3["Runtime::
Context


make_worker_group()"]:::neighbor - k4["Workers::
Group


submit()
close()
drain()"]:::focal + k2["Workers::
Pool


make_group(Workers::GroupOptions options): Workers::Group

shutdown()"]:::focal + k3["Runtime::
Context


make_worker_group(Workers::GroupOptions options):
Workers::Group"]:::neighbor + k4["Workers::
Group


submit(Work&& work): Result [future]

close()

drain()"]:::focal k5(["Application
CPU preparation"]):::app k6["Workers::
GroupOptions"]:::focal k7["Workers::
CpuPolicy"]:::focal - k8["Assets::Loading::
Loader


prepare()"]:::neighbor - k9["Runtime::PlatformDispatcher::
Submission


submit()"]:::neighbor + k8["Assets::Loading::
Loader


prepare(): Assets::Loading::Prepared"]:::neighbor + k9["Runtime::PlatformDispatcher::
Submission


submit(Work&& work): Result [future]"]:::neighbor k10(["Application
cleanup / quiesce()"]):::app k0 --> k1 k1 --> k2 diff --git a/docs/site/generation/diagram_catalog.py b/docs/site/generation/diagram_catalog.py index e700c82d..20b77d92 100644 --- a/docs/site/generation/diagram_catalog.py +++ b/docs/site/generation/diagram_catalog.py @@ -13,6 +13,13 @@ IDENTIFIER = re.compile(r"[a-z][a-z0-9-]*") +def label_text(value: str, *, markup: bool = False) -> str: + """Compare label content independently of HTML wrapping and whitespace.""" + if markup: + value = unescape(re.sub(r"<[^>]*>", "", value)) + return re.sub(r"\s+", "", value) + + def frame_version(directory: Path) -> str: content = b"".join( (directory / name).read_bytes() @@ -79,8 +86,17 @@ def load_catalog(source: Path) -> dict: path = Path(operation["file"]) if path.is_absolute() or ".." in path.parts or operation["line"] < 1: raise ValueError(f"Invalid operation source in {identity}: {name}") - if operation["label"] not in unescape(labels[str(keys.index(key))]): + if "CE::" in operation["label"] or "std::" in operation["label"]: + raise ValueError(f"Invalid operation display type in {identity}: {name}") + if label_text(operation["label"]) not in label_text(labels[str(keys.index(key))], markup=True): raise ValueError(f"Engine diagram {identity} omits operation label: {name}") + for key, label in diagram.get("operation_labels", {}).items(): + if key not in keys or symbols[key].get("kind") != "operation": + raise ValueError(f"Invalid operation box in Engine diagram {identity}: {key}") + if "CE::" in label or "std::" in label: + raise ValueError(f"Invalid operation display type in {identity}: {key}") + if label_text(label) != label_text(labels[str(keys.index(key))], markup=True): + raise ValueError(f"Engine diagram {identity} operation box label differs: {key}") for symbol in symbols.values(): if symbol["label"].startswith("CE::") or "std::" in symbol["label"]: raise ValueError(f"Unqualified Engine diagram label policy: {symbol['label']}") @@ -104,7 +120,10 @@ def write_catalog(source: Path, destination: Path, objects, get_target_uri, sour diagram["source"] = (source / (diagram["id"] + ".mmd")).read_text(encoding="utf-8") diagram["nodes"] = [deepcopy(catalog["symbols"][key]) for key in diagram["nodes"]] operations = diagram.pop("operations", {}) + operation_labels = diagram.pop("operation_labels", {}) for node in diagram["nodes"]: + if node["key"] in operation_labels: + node["label"] = operation_labels[node["key"]] if node["key"] in operations: node["operations"] = operations[node["key"]] for item in [node, *node.get("operations", [])]: diff --git a/docs/site/generation/static/engine-diagrams/explorer.mjs b/docs/site/generation/static/engine-diagrams/explorer.mjs index 9f32a084..f04a529f 100644 --- a/docs/site/generation/static/engine-diagrams/explorer.mjs +++ b/docs/site/generation/static/engine-diagrams/explorer.mjs @@ -127,7 +127,7 @@ window.galleryState = { (current.system === 'overview' ? '

Each box is a documented responsibility. Click it to open that subsystem. Click an arrow for supporting types.

' : '
Types in this subsystem
Other Engine contracts
Illustrative application roles

Named free functions appear as operation boxes where they form the actual API boundary. Rounded cream boxes are roles, not Engine types.

' + - (current.view === 'application' ? '

Methods beneath a type name are selected application entry points. “Implement hooks” identifies methods you override for the engine to call. Select the box for method contracts, reference links and Accepts/Returns links to the types shown in this view.

' : '')) + + (current.view === 'application' ? '

Methods beneath a type name are selected application entry points. “Implement hooks” identifies methods you override for the engine to call. Select the box for method contracts, reference links and Accepts/Returns links to the types shown in this view.

Signatures use method(arguments): ResultType. [] marks collections; bracketed notes retain ownership, future or borrowing information. Scalar and standard-library details are abridged; a missing result suffix does not imply void.

' : '')) + '

Arrows

Consumer → dependency. No chronological order is implied. Dashed arrows mark optional or alternative choices.

Scope and evidence

These are curated maps. Supporting fields/enums and unrelated dependencies are omitted. The C++ API reference supplies the complete declaration contracts.

All Engine arrows have declaration or implementation evidence. Application wiring is illustrative and follows the subsystem contracts.

' + current.refs.map(sourceLink).join('

') + '

Source revision ' + data.metadata.revision.slice(0, 12) + diff --git a/docs/site/generation/tests/test_engine_diagrams.py b/docs/site/generation/tests/test_engine_diagrams.py index 67576d93..dd65c984 100644 --- a/docs/site/generation/tests/test_engine_diagrams.py +++ b/docs/site/generation/tests/test_engine_diagrams.py @@ -85,6 +85,18 @@ def test_method_labels_must_appear_in_the_owning_box(self): self.write(catalog, SOURCE.replace('Runtime::Context"]', 'Runtime::Context
events()"]')) self.assertEqual(load_catalog(self.source), catalog) + def test_template_arguments_survive_label_markup_normalization(self): + catalog = fixture() + method = {**operation(), "label": "notify(Events::EventChannel& event)"} + catalog["diagrams"][0]["operations"] = {"Runtime::Context": [method]} + source = SOURCE.replace('Runtime::Context"]', + 'Runtime::Context
notify(
Events::EventChannel<Payload>& event)"]') + self.write(catalog, source) + self.assertEqual(load_catalog(self.source), catalog) + self.write(catalog, source.replace("Payload", "OtherPayload")) + with self.assertRaisesRegex(ValueError, "omits operation label"): + load_catalog(self.source) + def test_unknown_method_owner_is_rejected(self): catalog = fixture() catalog["diagrams"][0]["operations"] = {"Elsewhere": [operation()]} @@ -92,8 +104,43 @@ def test_unknown_method_owner_is_rejected(self): with self.assertRaisesRegex(ValueError, "Unknown operation owner"): load_catalog(self.source) + def test_wrapped_signature_keeps_argument_and_result_types(self): + catalog = fixture() + method = {**operation(), "label": "events(): Events::EventBus&"} + catalog["diagrams"][0]["operations"] = {"Runtime::Context": [method]} + source = SOURCE.replace('Runtime::Context"]', + 'Runtime::Context
events():
Events::EventBus&"]') + self.write(catalog, source) + self.assertEqual(load_catalog(self.source), catalog) + self.write(catalog, source.replace("Events::EventBus", "Events::EventChannel")) + with self.assertRaisesRegex(ValueError, "omits operation label"): + load_catalog(self.source) + class PublicationTests(unittest.TestCase): + def test_typed_free_operation_stays_specific_to_its_view(self): + with tempfile.TemporaryDirectory(prefix="cheryl-diagram-operation-") as directory: + source = Path(directory) / "source" + source.mkdir() + catalog = fixture() + key = "Runtime::Context" + catalog["symbols"][key]["kind"] = "operation" + catalog["symbols"][key]["label"] = "make_context()" + application = catalog["diagrams"][0] + application["operation_labels"] = {key: "make_context(): Runtime::Context [unique]"} + catalog["diagrams"].append({**fixture()["diagrams"][0], "id": "runtime-internal"}) + (source / "catalog.json").write_text(json.dumps(catalog)) + (source / "runtime-application.mmd").write_text(SOURCE.replace( + "Runtime::Context", "make_context():
Runtime::Context [unique]")) + (source / "runtime-internal.mmd").write_text(SOURCE.replace("Runtime::Context", "make_context()")) + destination = Path(directory) / "html" + write_catalog(source, destination, [], lambda name: name + ".html", "https://example.com") + output = json.loads((destination / "catalog.json").read_text()) + self.assertEqual(output["diagrams"][0]["nodes"][0]["label"], + "make_context(): Runtime::Context [unique]") + self.assertEqual(output["diagrams"][1]["nodes"][0]["label"], "make_context()") + self.assertEqual(catalog["symbols"][key]["label"], "make_context()") + def test_frame_assets_change_their_browser_version(self): with tempfile.TemporaryDirectory(prefix="cheryl-diagram-assets-") as directory: root = Path(directory) From 266762851ef6ef40d687d0525d32ed552f7af102 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Wed, 21 Oct 2026 12:37:39 -0700 Subject: [PATCH 94/98] Scopes Input diagram names to their subsystem Omits Input:: from the application diagram while preserving Actions:: and external namespaces. States the local scope beside the view question and in the Mermaid description. Keeps fully qualified inspector signatures, search identities and API targets. Adds optional per-view namespace validation and focused regression checks for namespace disambiguation and canonical inspector/API identity. Verification: 18 focused diagram tooling tests, JavaScript syntax and diff checks pass. Cached docs generation and browser verification follow. --- docs/site/development/documentation.md | 7 ++++ docs/site/engine-systems/diagram-explorer.md | 3 ++ .../site/engine-systems/diagrams/catalog.json | 7 ++-- .../diagrams/input-application.mmd | 22 +++++------ docs/site/generation/diagram_catalog.py | 15 +++++++- .../static/engine-diagrams/explorer.mjs | 3 +- .../generation/tests/test_engine_diagrams.py | 38 +++++++++++++++++++ 7 files changed, 78 insertions(+), 17 deletions(-) diff --git a/docs/site/development/documentation.md b/docs/site/development/documentation.md index 7376c2cb..9bdc5806 100644 --- a/docs/site/development/documentation.md +++ b/docs/site/development/documentation.md @@ -207,6 +207,13 @@ markers such as `[shared]`, `[unique]`, `[future]` or `[borrowed]` where hiding library wrapper would otherwise obscure ownership, asynchronous completion or borrowing. The API reference remains the complete signature. +A view can declare `namespace`, such as `"Input"`, to omit that prefix from +its Mermaid labels. Keep nested namespaces (`Actions::Context`) and other +namespaces (`Runtime::Context`) explicit. The explorer states this local scope +beside the view question; retain it in the Mermaid accessibility description +too. Keep catalog symbol and method labels fully qualified for the inspector, +search and API links. Label validation applies the same scoped abbreviation. + For a named free-function box, use the view's `operation_labels` mapping from its existing node key to the typed label. Keep the global symbol identity and other views' labels unchanged. Match this label in the Mermaid source; its diff --git a/docs/site/engine-systems/diagram-explorer.md b/docs/site/engine-systems/diagram-explorer.md index 27460e16..4d9da638 100644 --- a/docs/site/engine-systems/diagram-explorer.md +++ b/docs/site/engine-systems/diagram-explorer.md @@ -17,6 +17,9 @@ suffix can mean a scalar or standard-library result and does not imply `void`. `[]` marks a collection. Short notes such as `[shared]`, `[unique]`, `[future]` and `[borrowed]` retain ownership, asynchronous-result and borrowing information when their library wrappers are omitted. The API reference has the full types. +When a view names a local namespace, its prefix is omitted in the diagram: +within Input, `Actions::Context` means `Input::Actions::Context`. Other +namespaces remain explicit, and the inspector retains the full names. Application arrows include the argument and result types used by selected methods. In the inspector, **Accepts** and **Returns** connect a method to those diff --git a/docs/site/engine-systems/diagrams/catalog.json b/docs/site/engine-systems/diagrams/catalog.json index 92eaa143..55753413 100644 --- a/docs/site/engine-systems/diagrams/catalog.json +++ b/docs/site/engine-systems/diagrams/catalog.json @@ -15019,10 +15019,10 @@ } ], "notes": [ - "attach(ContextDefinition) returns a Context lease. Pass that lease to Tick::actions(Context) to obtain an Actions::View; the alternative overload accepts ContextId. ContextDefinition is setup data, not the lookup argument.", + "attach(Actions::ContextDefinition) returns an Actions::Context lease. Pass that lease to Tick::actions(Actions::Context) to obtain an Actions::View; the alternative overload accepts Actions::ContextId. Actions::ContextDefinition is setup data, not the lookup argument.", "Direct binding changes run during platform initialization. Reusable controllers retain a context endpoint and their own lease.", "Updates consume semantic actions and copied observations; text input/focus is a separate focused view.", - "Acquire Context::input(), action_contexts() and keyboard_id() on platform during initialization; use retained endpoints and immutable tick actions afterward." + "Acquire Runtime::Context::input(), action_contexts() and keyboard_id() on platform during initialization; use retained endpoints and immutable tick actions afterward." ], "level": "detailed", "refs": [ @@ -15179,7 +15179,8 @@ "role": "call" } ] - } + }, + "namespace": "Input" }, { "id": "assets-application", diff --git a/docs/site/engine-systems/diagrams/input-application.mmd b/docs/site/engine-systems/diagrams/input-application.mmd index dff7e0da..a7b377c7 100644 --- a/docs/site/engine-systems/diagrams/input-application.mmd +++ b/docs/site/engine-systems/diagrams/input-application.mmd @@ -13,21 +13,21 @@ config: --- flowchart LR accTitle: Input · application actions and controllers - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Input::. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Runtime::
Context


input(): Input::System&"]:::neighbor - k2["Input::
System


bindings(): Input::Actions::Bindings&

action_contexts(): Input::Actions::ContextSubmission

keyboard_id(): Input::DeviceId"]:::focal - k3["Input::Actions::
Bindings


bind_button(
Input::Control control,
Input::Actions::Id action):
Input::Actions::BindingId

bind_axis(
Input::Control axis,
Input::Actions::Id action,
Input::Actions::AxisOptions options):
Input::Actions::BindingId

unbind(Input::Actions::BindingId binding)"]:::focal - k4["Input::
Control"]:::focal - k5["Input::
KeyboardKey"]:::focal + k1["Runtime::
Context


input(): System&"]:::neighbor + k2["System

bindings(): Actions::Bindings&

action_contexts(): Actions::ContextSubmission

keyboard_id(): DeviceId"]:::focal + k3["Actions::
Bindings


bind_button(
Control control,
Actions::Id action):
Actions::BindingId

bind_axis(
Control axis,
Actions::Id action,
Actions::AxisOptions options):
Actions::BindingId

unbind(Actions::BindingId binding)"]:::focal + k4["Control"]:::focal + k5["KeyboardKey"]:::focal k6(["Application-owned controller"]):::app - k7["Input::Actions::
ContextDefinition"]:::focal - k8["Input::Actions::
ContextSubmission


attach(Input::Actions::ContextDefinition definition):
Input::Actions::Context"]:::focal - k9["Input::Actions::
Context


id(): Input::Actions::ContextId

reset()"]:::focal + k7["Actions::
ContextDefinition"]:::focal + k8["Actions::
ContextSubmission


attach(Actions::ContextDefinition definition):
Actions::Context"]:::focal + k9["Actions::
Context


id(): Actions::ContextId

reset()"]:::focal k10(["Application
update()"]):::app k11["Game::
TickContext"]:::neighbor - k12["Input::
Tick


actions(const Input::Actions::Context& context):
Input::Actions::View [borrowed]

button(Input::Actions::Id action):
Input::Actions::ButtonTickState

axis(Input::Actions::Id action):
Input::Actions::AxisTickState"]:::focal - k13["Input::Actions::
View


button(Input::Actions::Id action):
Input::Actions::ButtonTickState

axis(Input::Actions::Id action):
Input::Actions::AxisTickState"]:::focal + k12["Tick

actions(const Actions::Context& context):
Actions::View [borrowed]

button(Actions::Id action):
Actions::ButtonTickState

axis(Actions::Id action):
Actions::AxisTickState"]:::focal + k13["Actions::
View


button(Actions::Id action):
Actions::ButtonTickState

axis(Actions::Id action):
Actions::AxisTickState"]:::focal k2 --> k3 k3 --> k4 k4 --> k5 diff --git a/docs/site/generation/diagram_catalog.py b/docs/site/generation/diagram_catalog.py index 20b77d92..7aad3607 100644 --- a/docs/site/generation/diagram_catalog.py +++ b/docs/site/generation/diagram_catalog.py @@ -20,6 +20,13 @@ def label_text(value: str, *, markup: bool = False) -> str: return re.sub(r"\s+", "", value) +def scoped_label(value: str, namespace: str | None) -> str: + """Elide an explicitly named local namespace without changing type identity.""" + if not namespace: + return value + return re.sub(r"(? str: content = b"".join( (directory / name).read_bytes() @@ -49,6 +56,9 @@ def load_catalog(source: Path) -> dict: if not IDENTIFIER.fullmatch(identity) or identity in seen: raise ValueError(f"Invalid or duplicate Engine diagram ID: {identity}") seen.add(identity) + namespace = diagram.get("namespace") + if namespace is not None and not re.fullmatch(r"[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*", namespace): + raise ValueError(f"Invalid local namespace in Engine diagram {identity}") keys = diagram["nodes"] if len(set(keys)) != len(keys) or any(key not in symbols for key in keys): raise ValueError(f"Unknown or duplicate node in Engine diagram {identity}") @@ -88,14 +98,15 @@ def load_catalog(source: Path) -> dict: raise ValueError(f"Invalid operation source in {identity}: {name}") if "CE::" in operation["label"] or "std::" in operation["label"]: raise ValueError(f"Invalid operation display type in {identity}: {name}") - if label_text(operation["label"]) not in label_text(labels[str(keys.index(key))], markup=True): + display = scoped_label(operation["label"], namespace) + if label_text(display) not in label_text(labels[str(keys.index(key))], markup=True): raise ValueError(f"Engine diagram {identity} omits operation label: {name}") for key, label in diagram.get("operation_labels", {}).items(): if key not in keys or symbols[key].get("kind") != "operation": raise ValueError(f"Invalid operation box in Engine diagram {identity}: {key}") if "CE::" in label or "std::" in label: raise ValueError(f"Invalid operation display type in {identity}: {key}") - if label_text(label) != label_text(labels[str(keys.index(key))], markup=True): + if label_text(scoped_label(label, namespace)) != label_text(labels[str(keys.index(key))], markup=True): raise ValueError(f"Engine diagram {identity} operation box label differs: {key}") for symbol in symbols.values(): if symbol["label"].startswith("CE::") or "std::" in symbol["label"]: diff --git a/docs/site/generation/static/engine-diagrams/explorer.mjs b/docs/site/generation/static/engine-diagrams/explorer.mjs index f04a529f..e9ca82d7 100644 --- a/docs/site/generation/static/engine-diagrams/explorer.mjs +++ b/docs/site/generation/static/engine-diagrams/explorer.mjs @@ -286,7 +286,8 @@ window.galleryState = { : current.level === 'detailed' ? 'System relationships' : current.level + ' view'); $('#title').textContent = current.title; - $('#question').textContent = current.question; + $('#question').textContent = current.question + + (current.namespace ? ' Local names omit ' + current.namespace + ':: in the diagram.' : ''); $('#counts').textContent = current.nodes.length + ' boxes · ' + current.edges.length + ' arrows'; document.querySelectorAll('.nav-button') diff --git a/docs/site/generation/tests/test_engine_diagrams.py b/docs/site/generation/tests/test_engine_diagrams.py index dd65c984..672b2ebd 100644 --- a/docs/site/generation/tests/test_engine_diagrams.py +++ b/docs/site/generation/tests/test_engine_diagrams.py @@ -116,8 +116,46 @@ def test_wrapped_signature_keeps_argument_and_result_types(self): with self.assertRaisesRegex(ValueError, "omits operation label"): load_catalog(self.source) + def test_local_namespace_preserves_nested_and_external_qualifiers(self): + catalog = fixture() + method = {**operation(), "label": "attach(Runtime::Context next, Input::Actions::Context actions): Runtime::Context"} + diagram = catalog["diagrams"][0] + diagram["namespace"] = "Runtime" + diagram["operations"] = {"Runtime::Context": [method]} + source = SOURCE.replace('Runtime::Context"]', + 'Context
attach(Context next, Input::Actions::Context actions): Context"]') + self.write(catalog, source) + self.assertEqual(load_catalog(self.source), catalog) + self.write(catalog, source.replace("Input::Actions::Context", "Input::Context")) + with self.assertRaisesRegex(ValueError, "omits operation label"): + load_catalog(self.source) + class PublicationTests(unittest.TestCase): + def test_local_namespace_keeps_full_inspector_and_api_identity(self): + with tempfile.TemporaryDirectory(prefix="cheryl-diagram-namespace-") as directory: + source = Path(directory) / "source" + source.mkdir() + catalog = fixture() + method = {**operation(), "label": "context(): Runtime::Context&"} + diagram = catalog["diagrams"][0] + diagram["namespace"] = "Runtime" + diagram["operations"] = {"Runtime::Context": [method]} + (source / "catalog.json").write_text(json.dumps(catalog)) + (source / "runtime-application.mmd").write_text(SOURCE.replace( + 'Runtime::Context"]', 'Context
context(): Context&"]')) + destination = Path(directory) / "html" + objects = [("CE::Runtime::Context", "", "class", "api/context", "type", 1), + (method["name"], "", "function", "api/context", "method", 1)] + write_catalog(source, destination, objects, lambda name: name + ".html", "https://example.com") + output = json.loads((destination / "catalog.json").read_text())["diagrams"][0] + self.assertEqual(output["namespace"], "Runtime") + node = output["nodes"][0] + self.assertEqual(node["label"], "Runtime::Context") + self.assertEqual(node["api"], "api/context.html#type") + self.assertEqual(node["operations"][0]["label"], method["label"]) + self.assertEqual(node["operations"][0]["api"], "api/context.html#method") + def test_typed_free_operation_stays_specific_to_its_view(self): with tempfile.TemporaryDirectory(prefix="cheryl-diagram-operation-") as directory: source = Path(directory) / "source" From 37815c1a25a2a8665f6b1bf07551ea05e6d1e981 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Wed, 21 Oct 2026 13:52:32 -0700 Subject: [PATCH 95/98] Simplifies signatures and namespace labels across subsystem diagrams Removes typed parameter identifiers from application diagram signatures while retaining types, return values and meaningful ownership markers. Preserves full signatures in the inspector, search and API reference links. Applies local namespace scopes across subsystem views, including diagrams embedded in guides. Preserves ambiguous nested namespaces such as Text::Rendering and Input::Events, and keeps mixed diagnostics and utility views qualified. Leaves type identities, node inventories and arrows intact. Verification: 20 focused diagram tooling tests, JavaScript syntax and diff checks pass. Independent reviews verify all 185 application signatures and the namespace scopes. Cached docs generation and browser checks follow. --- docs/site/development/documentation.md | 18 ++- docs/site/engine-systems/diagram-explorer.md | 8 +- .../diagrams/assets-application.mmd | 18 +-- .../diagrams/assets-focus-loading.mmd | 10 +- .../diagrams/assets-focus-playback.mmd | 8 +- .../diagrams/assets-internal.mmd | 20 +-- .../diagrams/audio-application.mmd | 20 +-- .../diagrams/audio-internal.mmd | 20 +-- .../site/engine-systems/diagrams/catalog.json | 138 ++++++++++++------ .../diagrams/dispatch-application.mmd | 16 +- .../diagrams/dispatch-internal.mmd | 14 +- .../diagrams/display-application.mmd | 20 +-- .../diagrams/display-internal.mmd | 16 +- .../diagrams/events-application.mmd | 18 +-- .../diagrams/events-focus-delivery.mmd | 14 +- .../diagrams/events-focus-immediate.mmd | 8 +- .../diagrams/events-internal.mmd | 22 +-- .../diagrams/gui-application.mmd | 18 +-- .../diagrams/gui-focus-rmlui.mmd | 10 +- .../diagrams/gui-focus-tgui.mmd | 10 +- .../engine-systems/diagrams/gui-internal.mmd | 18 +-- .../diagrams/input-application.mmd | 10 +- .../diagrams/input-focus-actions.mmd | 16 +- .../diagrams/input-focus-capture.mmd | 12 +- .../diagrams/input-internal.mmd | 24 +-- .../diagrams/logging-application.mmd | 24 +-- .../diagrams/logging-internal.mmd | 18 +-- .../diagrams/memory-application.mmd | 6 +- .../diagrams/memory-internal.mmd | 24 +-- .../diagrams/rendering-application.mmd | 16 +- .../diagrams/rendering-focus-playback.mmd | 14 +- .../diagrams/rendering-focus-sprite.mmd | 16 +- .../diagrams/rendering-internal.mmd | 30 ++-- .../diagrams/resources-application.mmd | 12 +- .../diagrams/resources-internal.mmd | 12 +- .../diagrams/runtime-application.mmd | 18 +-- .../diagrams/runtime-focus-assembly.mmd | 6 +- .../diagrams/runtime-focus-frame.mmd | 4 +- .../diagrams/runtime-focus-shutdown.mmd | 18 +-- .../diagrams/runtime-internal.mmd | 10 +- .../diagrams/state-application.mmd | 12 +- .../diagrams/state-internal.mmd | 14 +- .../diagrams/text-application.mmd | 20 +-- .../diagrams/text-focus-static.mmd | 10 +- .../diagrams/text-focus-updater.mmd | 2 +- .../engine-systems/diagrams/text-internal.mmd | 12 +- .../diagrams/utilities-application.mmd | 6 +- .../diagrams/workers-application.mmd | 14 +- .../diagrams/workers-focus-groups.mmd | 8 +- .../diagrams/workers-focus-shared.mmd | 8 +- .../diagrams/workers-internal.mmd | 20 +-- docs/site/generation/diagram_catalog.py | 52 ++++++- .../generation/extensions/engine_diagrams.py | 2 + .../static/engine-diagrams/explorer.mjs | 2 +- .../generation/tests/test_engine_diagrams.py | 39 ++++- 55 files changed, 546 insertions(+), 409 deletions(-) diff --git a/docs/site/development/documentation.md b/docs/site/development/documentation.md index 9bdc5806..44792199 100644 --- a/docs/site/development/documentation.md +++ b/docs/site/development/documentation.md @@ -187,8 +187,9 @@ public methods. Keep these choices specific to the view: usually one to three entry points per type, with value types left as values. Each entry records the qualified declaration name, short display label, source file/line, purpose and `role` (`call` or `override`). Use the declaring base type for inherited methods. -Include the same labels beneath the owning type in the Mermaid source; label -checks reject stale metadata. Wrapping markup may change whitespace, but must +Include the corresponding signatures beneath the owning type in Mermaid, +omitting parameter names when their types are shown; label checks reject stale +metadata. Wrapping markup may change whitespace, but must preserve type spelling, including template arguments. Mark override groups **Implement hooks** to distinguish engine callbacks from application calls. The explorer adds method search, source links and unambiguous API links without @@ -197,8 +198,9 @@ focused views. Write labels as `method(arguments): QualifiedResultType`. Include relevant Engine argument and result types, including public aliases and types without -their own box. Keep argument names and the declaration's direct `const`, pointer -and reference qualifiers. Omit defaults, the leading `CE::`, and primitive or +their own box. Keep argument names in catalog labels for the inspector, and +omit those names from Mermaid when their types are shown. Preserve the declaration's +direct `const`, pointer and reference qualifiers. Omit defaults, the leading `CE::`, and primitive or `std::` type spellings. Retain names for scalar and library-only arguments; include template parameter types where they clarify generic arguments. A missing result suffix does not imply `void`: scalar and @@ -212,11 +214,15 @@ its Mermaid labels. Keep nested namespaces (`Actions::Context`) and other namespaces (`Runtime::Context`) explicit. The explorer states this local scope beside the view question; retain it in the Mermaid accessibility description too. Keep catalog symbol and method labels fully qualified for the inspector, -search and API links. Label validation applies the same scoped abbreviation. +search and API links. Preserve a local prefix when shortening it would collide +with another Engine namespace, such as `Text::Rendering` or `Input::Events`. +Label validation applies the same namespace and parameter-name abbreviations. +Mixed views such as diagnostics and utilities keep their namespaces explicit. For a named free-function box, use the view's `operation_labels` mapping from its existing node key to the typed label. Keep the global symbol identity and -other views' labels unchanged. Match this label in the Mermaid source; its +other views' labels unchanged. Apply the same parameter-name and namespace +abbreviations in the Mermaid source; its validation also ignores wrapping whitespace while preserving template types. Connect selected methods to their public Engine argument and result types when diff --git a/docs/site/engine-systems/diagram-explorer.md b/docs/site/engine-systems/diagram-explorer.md index 4d9da638..7e4aa1e0 100644 --- a/docs/site/engine-systems/diagram-explorer.md +++ b/docs/site/engine-systems/diagram-explorer.md @@ -12,14 +12,18 @@ to call. Select a box for each method's purpose, source and available API link; search also finds method names. Signatures use `method(arguments): ResultType`, with qualified Engine types and -public aliases for arguments and results. They are abridged: a missing result +public aliases for arguments and results. Diagram signatures omit parameter +names when their types are shown; the inspector keeps the fuller signatures. +They are abridged: a missing result suffix can mean a scalar or standard-library result and does not imply `void`. `[]` marks a collection. Short notes such as `[shared]`, `[unique]`, `[future]` and `[borrowed]` retain ownership, asynchronous-result and borrowing information when their library wrappers are omitted. The API reference has the full types. When a view names a local namespace, its prefix is omitted in the diagram: within Input, `Actions::Context` means `Input::Actions::Context`. Other -namespaces remain explicit, and the inspector retains the full names. +namespaces remain explicit, as do local names that would collide with another +namespace, such as `Text::Rendering` and `Input::Events`. Mixed diagnostics +and utility views keep their namespaces; the inspector retains full names. Application arrows include the argument and result types used by selected methods. In the inspector, **Accepts** and **Returns** connect a method to those diff --git a/docs/site/engine-systems/diagrams/assets-application.mmd b/docs/site/engine-systems/diagrams/assets-application.mmd index 23cb133e..f147db77 100644 --- a/docs/site/engine-systems/diagrams/assets-application.mmd +++ b/docs/site/engine-systems/diagrams/assets-application.mmd @@ -13,21 +13,21 @@ config: --- flowchart LR accTitle: Assets · application loading and playback - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
loading work"]):::app - k1["Assets::Loading::
Loader


load_assets(Rendering::Resources::Provider& provider)

prepare(): Assets::Loading::Prepared

upload(
Assets::Loading::Prepared prepared,
Rendering::Resources::Provider& provider,
replace_shaders)"]:::focal - k2["Assets::Loading::
Prepared"]:::focal + k1["Loading::
Loader


load_assets(Rendering::Resources::Provider&)

prepare(): Loading::Prepared

upload(
Loading::Prepared,
Rendering::Resources::Provider&,
replace_shaders)"]:::focal + k2["Loading::
Prepared"]:::focal k3(["Application
init_platform()"]):::app k4["Rendering::Resources::
Provider"]:::neighbor k5(["Application-owned game objects"]):::app - k6["Assets::Sprites::
Manager


get_asset(name_space, name):
Assets::Sprites::Manager::Handle [shared]"]:::focal - k7["Assets::Tilesets::
Manager


get_asset(key):
Assets::Tilesets::Manager::Handle [shared]"]:::focal - k8["Assets::Sprites::
Asset


animation(animation, facing): Assets::Sprites::Animation"]:::focal - k9["Assets::Tilesets::
Asset


tile(cell): Assets::Tilesets::Tile

animation(name): Assets::Tilesets::Animation

cell_at(Assets::Grids::CellIndex target, elapsed):
Assets::Grids::CellIndex"]:::focal + k6["Sprites::
Manager


get_asset(name_space, name):
Sprites::Manager::Handle [shared]"]:::focal + k7["Tilesets::
Manager


get_asset(key): Tilesets::Manager::Handle [shared]"]:::focal + k8["Sprites::
Asset


animation(animation, facing): Sprites::Animation"]:::focal + k9["Tilesets::
Asset


tile(cell): Tilesets::Tile

animation(name): Tilesets::Animation

cell_at(Grids::CellIndex, elapsed): Grids::CellIndex"]:::focal k10(["Application
update()"]):::app - k11["Assets::Sprites::
Animation


advance(elapsed)

cell(): Assets::Grids::CellIndex"]:::focal + k11["Sprites::
Animation


advance(elapsed)

cell(): Grids::CellIndex"]:::focal k12(["Application
prepare_render_frame()"]):::app - k13("Rendering::Submission::resolve_sprite(
const Assets::Sprites::Asset& sprite,
Assets::Grids::CellIndex cell,
const Rendering::DrawStyle2D& style,
const Rendering::Submission::Context2D& context):
Rendering::DrawPacket2D"):::neighbor + k13("Rendering::Submission::resolve_sprite(
const Sprites::Asset&,
Grids::CellIndex,
const Rendering::DrawStyle2D&,
const Rendering::Submission::Context2D&):
Rendering::DrawPacket2D"):::neighbor k14["Rendering::
DrawPacket2D"]:::neighbor k0 --> k1 k1 --> k2 diff --git a/docs/site/engine-systems/diagrams/assets-focus-loading.mmd b/docs/site/engine-systems/diagrams/assets-focus-loading.mmd index 9d6afd05..dbb1f227 100644 --- a/docs/site/engine-systems/diagrams/assets-focus-loading.mmd +++ b/docs/site/engine-systems/diagrams/assets-focus-loading.mmd @@ -13,14 +13,14 @@ config: --- flowchart LR accTitle: Assets · simple loading and lookup - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Assets::Loading::
Loader"]:::focal - k2["Assets::Loading::
Prepared"]:::focal + k1["Loading::
Loader"]:::focal + k2["Loading::
Prepared"]:::focal k3["Rendering::Resources::
Provider"]:::neighbor k4(["Application-owned game objects"]):::app - k5["Assets::Sprites::
Manager"]:::focal - k6["Assets::Sprites::
Asset"]:::focal + k5["Sprites::
Manager"]:::focal + k6["Sprites::
Asset"]:::focal k0 --> k1 k1 --> k2 k1 --> k3 diff --git a/docs/site/engine-systems/diagrams/assets-focus-playback.mmd b/docs/site/engine-systems/diagrams/assets-focus-playback.mmd index 6e59d90f..d00c6879 100644 --- a/docs/site/engine-systems/diagrams/assets-focus-playback.mmd +++ b/docs/site/engine-systems/diagrams/assets-focus-playback.mmd @@ -13,12 +13,12 @@ config: --- flowchart TB accTitle: Assets · practical shared sprite, separate playback - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application-owned game objects"]):::app - k1["Assets::Sprites::
Asset"]:::focal + k1["Sprites::
Asset"]:::focal k2(["Application
update()"]):::app - k3["Assets::Sprites::
Animation"]:::focal - k4["Assets::Sprites::
Clip"]:::focal + k3["Sprites::
Animation"]:::focal + k4["Sprites::
Clip"]:::focal k5(["Application
prepare_render_frame()"]):::app k6("Rendering::Submission::
resolve_sprite()"):::neighbor k7["Rendering::
DrawStyle2D"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/assets-internal.mmd b/docs/site/engine-systems/diagrams/assets-internal.mmd index 05671b68..eb9dac07 100644 --- a/docs/site/engine-systems/diagrams/assets-internal.mmd +++ b/docs/site/engine-systems/diagrams/assets-internal.mmd @@ -13,25 +13,25 @@ config: --- flowchart LR accTitle: Assets · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. - k0["Assets::Loading::
Loader"]:::focal - k1["Assets::Loading::
FileRegistry"]:::focal + accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + k0["Loading::
Loader"]:::focal + k1["Loading::
FileRegistry"]:::focal k2["Files::
FileIndex"]:::neighbor - k3["Assets::Loading::
Prepared"]:::focal + k3["Loading::
Prepared"]:::focal k4["Rendering::Resources::
Provider"]:::neighbor - k5["Assets::Sprites::
Manager"]:::focal - k6["Assets::Tilesets::
Manager"]:::focal - k7["Assets::Cache::
Store"]:::focal - k8["Assets::Cache::
AssetCatalogue"]:::focal + k5["Sprites::
Manager"]:::focal + k6["Tilesets::
Manager"]:::focal + k7["Cache::
Store"]:::focal + k8["Cache::
AssetCatalogue"]:::focal k9["Memory::Objects::
Reservation"]:::neighbor k10["Memory::Allocators::
ObjectPoolAllocator"]:::neighbor k11["Memory::Objects::
Pool"]:::neighbor - k12["Assets::Sprites::
Asset"]:::focal + k12["Sprites::
Asset"]:::focal k13["Rendering::Resources::
Image"]:::neighbor k14["Rendering::Geometry::
Buffer2D"]:::neighbor k15("Rendering::Submission::
resolve_sprite()"):::neighbor k16("Rendering::Submission::
resolve_tile()"):::neighbor - k17["Assets::Tilesets::
Asset"]:::focal + k17["Tilesets::
Asset"]:::focal k18["Rendering::
DrawPacket2D"]:::neighbor k0 --> k1 k0 --> k2 diff --git a/docs/site/engine-systems/diagrams/audio-application.mmd b/docs/site/engine-systems/diagrams/audio-application.mmd index f1992141..181fb5a6 100644 --- a/docs/site/engine-systems/diagrams/audio-application.mmd +++ b/docs/site/engine-systems/diagrams/audio-application.mmd @@ -13,20 +13,20 @@ config: --- flowchart TB accTitle: Audio · application-owned playback and outcomes - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Audio::; ambiguous nested namespaces remain qualified. Local names omit Audio::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app - k1["Audio::Miniaudio::
System


open_device(
Audio::Format output,
Audio::PlaybackNotificationOptions notifications):
Audio::Miniaudio::System [unique]

publish_events_to(Events::EventBus::Publisher publisher)

pump_with(const Runtime::RuntimeServices& services):
Runtime::ServiceAttachment

publish_notifications()"]:::focal + k1["Miniaudio::
System


open_device(Format, PlaybackNotificationOptions):
Miniaudio::System [unique]

publish_events_to(Events::EventBus::Publisher)

pump_with(const Runtime::RuntimeServices&):
Runtime::ServiceAttachment

publish_notifications()"]:::focal k2(["Application
loading work"]):::app - k3("Audio::Miniaudio::decode_file(
file,
Audio::Miniaudio::DecodeOptions options):
Audio::Clip"):::focal - k4["Audio::
Clip


from_samples(Audio::Format format, samples): Audio::Clip"]:::focal + k3("Miniaudio::decode_file(file, Miniaudio::DecodeOptions):
Clip"):::focal + k4["Clip

from_samples(Format, samples): Clip"]:::focal k5(["Application
sound controller"]):::app - k6["Audio::
System


play(
const Audio::Clip& clip,
Audio::PlaybackOptions options):
Audio::Voice [shared]

stream(file, Audio::PlaybackOptions options):
Audio::Voice [shared]

maintain()

close()"]:::focal - k7["Audio::
PlaybackOptions"]:::focal - k8["Audio::
Voice


snapshot(): Audio::VoiceSnapshot

pause()

resume()

stop()"]:::focal - k9["Audio::
VoiceSnapshot"]:::focal + k6["System

play(const Clip&, PlaybackOptions): Voice [shared]

stream(file, PlaybackOptions): Voice [shared]

maintain()

close()"]:::focal + k7["PlaybackOptions"]:::focal + k8["Voice

snapshot(): VoiceSnapshot

pause()

resume()

stop()"]:::focal + k9["VoiceSnapshot"]:::focal k10(["Application
listener setup"]):::app - k11["Events::
EventBus


register_listener(
const Events::EventChannel<Audio::PlaybackEnded>&
event,
Events::EventChannel<Audio::PlaybackEnded>::Callback
callback,
Events::EventBus::Delivery delivery,
Events::EventBus::ErrorHandler errors):
Events::EventBus::Registration

publisher(): Events::EventBus::Publisher"]:::neighbor - k12["Audio::
PlaybackEnded"]:::focal + k11["Events::
EventBus


register_listener(
const Events::EventChannel<PlaybackEnded>&,
Events::EventChannel<PlaybackEnded>::Callback,
Events::EventBus::Delivery,
Events::EventBus::ErrorHandler):
Events::EventBus::Registration

publisher(): Events::EventBus::Publisher"]:::neighbor + k12["PlaybackEnded"]:::focal k13(["Application
init_platform()"]):::app k14["Runtime::
RuntimeServices"]:::neighbor k15["Runtime::
ServiceAttachment


reset()"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/audio-internal.mmd b/docs/site/engine-systems/diagrams/audio-internal.mmd index c2feb9fe..cc8bb682 100644 --- a/docs/site/engine-systems/diagrams/audio-internal.mmd +++ b/docs/site/engine-systems/diagrams/audio-internal.mmd @@ -13,16 +13,16 @@ config: --- flowchart LR accTitle: Audio · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. - k0["Audio::Miniaudio::
System"]:::focal - k1["Audio::
System"]:::focal - k2["Audio::
Clip"]:::focal - k3["Audio::
PlaybackOptions"]:::focal - k4["Audio::
Voice"]:::focal - k5["Audio::
VoiceSnapshot"]:::focal - k6["Audio::
PlaybackNotifications"]:::focal - k7["Audio::
PlaybackEnded"]:::focal - k8["Audio::
PlaybackTerminal"]:::focal + accDescr: Local names omit Audio::; ambiguous nested namespaces remain qualified. Local names omit Audio::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + k0["Miniaudio::
System"]:::focal + k1["System"]:::focal + k2["Clip"]:::focal + k3["PlaybackOptions"]:::focal + k4["Voice"]:::focal + k5["VoiceSnapshot"]:::focal + k6["PlaybackNotifications"]:::focal + k7["PlaybackEnded"]:::focal + k8["PlaybackTerminal"]:::focal k9["Events::EventBus::
Publisher"]:::neighbor k10["Runtime::
RuntimeServices"]:::neighbor k11["Runtime::
ServiceAttachment"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/catalog.json b/docs/site/engine-systems/diagrams/catalog.json index 55753413..12d2b1ee 100644 --- a/docs/site/engine-systems/diagrams/catalog.json +++ b/docs/site/engine-systems/diagrams/catalog.json @@ -3656,7 +3656,8 @@ "Game::TickContext", "Rendering::FrameWriter", "Input::Tick" - ] + ], + "namespace": "Runtime" }, { "id": "display-internal", @@ -4138,7 +4139,8 @@ "Events::EventSystem", "Game::TickContext", "Rendering::Camera2D" - ] + ], + "namespace": "Platform" }, { "id": "input-internal", @@ -5187,7 +5189,8 @@ "Input::Actions::View", "UI::TGUI::Session", "UI::RmlUi::Session" - ] + ], + "namespace": "Input" }, { "id": "assets-internal", @@ -5771,7 +5774,8 @@ "Rendering::Submission::resolve_tile()", "Assets::Tilesets::Asset", "Rendering::DrawPacket2D" - ] + ], + "namespace": "Assets" }, { "id": "resources-internal", @@ -6652,7 +6656,8 @@ "Rendering::Resources::Pipeline", "Rendering::OpenGL::Pipeline", "Rendering::OpenGL::Program" - ] + ], + "namespace": "Rendering::Resources" }, { "id": "rendering-internal", @@ -7320,7 +7325,8 @@ "Rendering::Submission::resolve_sprite()", "Assets::Sprites::Asset", "Rendering::Submission::Context2D" - ] + ], + "namespace": "Rendering" }, { "id": "text-internal", @@ -7988,7 +7994,8 @@ "Runtime::PlatformDispatcher::Submission", "UI::TGUI::Session", "UI::RmlUi::Session" - ] + ], + "namespace": "Text" }, { "id": "gui-internal", @@ -9044,7 +9051,8 @@ "UI::TGUI::Scene", "UI::RmlUi::Scene", "Rendering::PassWriter" - ] + ], + "namespace": "UI" }, { "id": "audio-internal", @@ -9544,7 +9552,8 @@ "Events::EventBus::Publisher", "Runtime::RuntimeServices", "Runtime::ServiceAttachment" - ] + ], + "namespace": "Audio" }, { "id": "events-internal", @@ -10116,7 +10125,8 @@ "Runtime::SimulationDispatcher::Submission", "Events::worker_delivery()", "Workers::Group" - ] + ], + "namespace": "Events" }, { "id": "dispatch-internal", @@ -10847,7 +10857,8 @@ "UI::RmlUi::SceneUploader", "Events::platform_delivery()", "Events::simulation_delivery()" - ] + ], + "namespace": "Runtime" }, { "id": "workers-internal", @@ -11517,7 +11528,8 @@ "Workers::PolicyStrength", "Text::Rendering::Updater", "Events::worker_delivery()" - ] + ], + "namespace": "Workers" }, { "id": "memory-internal", @@ -11914,7 +11926,8 @@ "Memory::ByteReleaseContext", "State::Singleton", "Memory::Stats" - ] + ], + "namespace": "Memory" }, { "id": "state-internal", @@ -12093,7 +12106,8 @@ "State::VersionedSnapshot", "State::StateTracker", "State::StateMachine" - ] + ], + "namespace": "State" }, { "id": "logging-internal", @@ -12633,7 +12647,8 @@ "Logging::FileEventHandlers", "Logging::Level", "Logging::OverflowPolicy" - ] + ], + "namespace": "Logging" }, { "id": "diagnostics-internal", @@ -13991,7 +14006,8 @@ }, "operation_labels": { "Application::GLFWOpenGL::make_context()": "Application::GLFWOpenGL::make_context(const Application::GLFWOpenGL::Options& config): Runtime::Context [unique]" - } + }, + "namespace": "Runtime" }, { "id": "display-application", @@ -14527,7 +14543,8 @@ "role": "call" } ] - } + }, + "namespace": "Platform" }, { "id": "input-application", @@ -15651,7 +15668,8 @@ }, "operation_labels": { "Rendering::Submission::resolve_sprite()": "Rendering::Submission::resolve_sprite(const Assets::Sprites::Asset& sprite, Assets::Grids::CellIndex cell, const Rendering::DrawStyle2D& style, const Rendering::Submission::Context2D& context): Rendering::DrawPacket2D" - } + }, + "namespace": "Assets" }, { "id": "resources-application", @@ -16184,7 +16202,8 @@ }, "operation_labels": { "Assets::Images::decode()": "Assets::Images::decode(file): Assets::Images::Decoded" - } + }, + "namespace": "Rendering::Resources" }, { "id": "rendering-application", @@ -16568,7 +16587,8 @@ }, "operation_labels": { "Rendering::Submission::resolve_sprite()": "Rendering::Submission::resolve_sprite(const Assets::Sprites::Asset& sprite, Assets::Grids::CellIndex cell, const Rendering::DrawStyle2D& style, const Rendering::Submission::Context2D& context): Rendering::DrawPacket2D" - } + }, + "namespace": "Rendering" }, { "id": "text-application", @@ -17258,7 +17278,8 @@ "Text::Rendering::prepare()": "Text::Rendering::prepare(Text::ShapedText layout, Text::Rendering::RasterOptions options): Text::Rendering::Prepared", "Text::Rendering::upload()": "Text::Rendering::upload(Text::Rendering::Prepared prepared, Rendering::Resources::Provider& provider, const Rendering::Resources::Material material [optional shared]): Text::Rendering::Generation", "Rendering::Submission::resolve_text()": "Rendering::Submission::resolve_text(const Text::Rendering::Generation& text, const Rendering::DrawStyle2D& style, const Rendering::Submission::Context2D& context): Rendering::DrawPacket2D[]" - } + }, + "namespace": "Text" }, { "id": "gui-application", @@ -17999,7 +18020,8 @@ "role": "call" } ] - } + }, + "namespace": "UI" }, { "id": "audio-application", @@ -18618,7 +18640,8 @@ }, "operation_labels": { "Audio::Miniaudio::decode_file()": "Audio::Miniaudio::decode_file(file, Audio::Miniaudio::DecodeOptions options): Audio::Clip" - } + }, + "namespace": "Audio" }, { "id": "events-application", @@ -19126,7 +19149,8 @@ }, "operation_labels": { "Events::simulation_delivery()": "Events::simulation_delivery(Runtime::SimulationDispatcher::Submission endpoint): Events::EventBus::Delivery" - } + }, + "namespace": "Events" }, { "id": "dispatch-application", @@ -19598,7 +19622,8 @@ "role": "call" } ] - } + }, + "namespace": "Runtime" }, { "id": "workers-application", @@ -20032,7 +20057,8 @@ "role": "call" } ] - } + }, + "namespace": "Workers" }, { "id": "memory-application", @@ -20193,7 +20219,8 @@ "role": "call" } ] - } + }, + "namespace": "Memory" }, { "id": "state-application", @@ -20477,7 +20504,8 @@ "role": "call" } ] - } + }, + "namespace": "State" }, { "id": "logging-application", @@ -21080,7 +21108,8 @@ "role": "call" } ] - } + }, + "namespace": "Logging" }, { "id": "diagnostics-application", @@ -22330,7 +22359,8 @@ "Rendering::PresentationSurface", "Rendering::Renderer", "Rendering::Resources::Provider" - ] + ], + "namespace": "Runtime" }, { "id": "runtime-focus-frame", @@ -22839,7 +22869,8 @@ "Rendering::Pass", "Rendering::DrawPacket2D", "Rendering::Renderer" - ] + ], + "namespace": "Runtime" }, { "id": "runtime-focus-shutdown", @@ -23241,7 +23272,8 @@ "Runtime::RuntimeLifecycle", "@report", "Runtime::RuntimeLifecycleSnapshot" - ] + ], + "namespace": "Runtime" }, { "id": "input-focus-actions", @@ -23613,7 +23645,8 @@ "@update", "Input::Tick", "Input::Actions::ButtonTickState" - ] + ], + "namespace": "Input" }, { "id": "input-focus-capture", @@ -23920,7 +23953,8 @@ "Input::Routing::KeyboardPolicy", "Input::Events::Record", "Input::Events::Payload" - ] + ], + "namespace": "Input" }, { "id": "assets-focus-loading", @@ -24180,7 +24214,8 @@ "@world", "Assets::Sprites::Manager", "Assets::Sprites::Asset" - ] + ], + "namespace": "Assets" }, { "id": "assets-focus-playback", @@ -24430,7 +24465,8 @@ "Rendering::Submission::resolve_sprite()", "Rendering::DrawStyle2D", "Rendering::DrawPacket2D" - ] + ], + "namespace": "Assets" }, { "id": "rendering-focus-playback", @@ -24623,7 +24659,8 @@ "Rendering::DrawPacket2D", "Rendering::Resources::Material", "Rendering::Geometry::Buffer2D" - ] + ], + "namespace": "Rendering" }, { "id": "rendering-focus-sprite", @@ -24855,7 +24892,8 @@ "Assets::Sprites::Asset", "Rendering::DrawStyle2D", "Rendering::DrawPacket2D" - ] + ], + "namespace": "Rendering" }, { "id": "text-focus-static", @@ -25072,7 +25110,8 @@ "Text::Rendering::upload()", "Rendering::Resources::Provider", "Text::Rendering::Generation" - ] + ], + "namespace": "Text" }, { "id": "text-focus-updater", @@ -25378,7 +25417,8 @@ "@prepare", "Rendering::Submission::resolve_text()", "Rendering::DrawPacket2D" - ] + ], + "namespace": "Text" }, { "id": "gui-focus-tgui", @@ -25944,7 +25984,8 @@ "UI::TGUI::Scene", "@prepare", "Rendering::PassWriter" - ] + ], + "namespace": "UI" }, { "id": "gui-focus-rmlui", @@ -26510,7 +26551,8 @@ "UI::RmlUi::Scene", "@prepare", "Rendering::PassWriter" - ] + ], + "namespace": "UI" }, { "id": "events-focus-immediate", @@ -26744,7 +26786,8 @@ "Events::EventBus::Registration", "@callback", "@cleanup" - ] + ], + "namespace": "Events" }, { "id": "events-focus-delivery", @@ -26943,7 +26986,8 @@ "Runtime::SimulationDispatcher::Submission", "Workers::Group", "Events::EventBus::ErrorHandler" - ] + ], + "namespace": "Events" }, { "id": "workers-focus-groups", @@ -27225,7 +27269,8 @@ "Workers::Group", "@cpu", "Workers::GroupOptions" - ] + ], + "namespace": "Workers" }, { "id": "workers-focus-shared", @@ -27731,7 +27776,8 @@ "Runtime::Context#context B", "Workers::Group#group A", "Workers::Group#group B" - ] + ], + "namespace": "Workers" }, { "id": "overview-systems", diff --git a/docs/site/engine-systems/diagrams/dispatch-application.mmd b/docs/site/engine-systems/diagrams/dispatch-application.mmd index 2b18bedb..5becbe11 100644 --- a/docs/site/engine-systems/diagrams/dispatch-application.mmd +++ b/docs/site/engine-systems/diagrams/dispatch-application.mmd @@ -13,18 +13,18 @@ config: --- flowchart LR accTitle: Dispatch · application owner requests - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Runtime::
Context


platform_dispatcher(): Runtime::PlatformDispatcher&

resources(): Rendering::Resources::Provider&"]:::neighbor - k2["Runtime::
PlatformDispatcher


submission(): Runtime::PlatformDispatcher::Submission"]:::focal - k3["Runtime::PlatformDispatcher::
Submission


submit(Work&& work): Result [future]"]:::focal + k1["Context

platform_dispatcher(): PlatformDispatcher&

resources(): Rendering::Resources::Provider&"]:::neighbor + k2["PlatformDispatcher

submission(): PlatformDispatcher::Submission"]:::focal + k3["PlatformDispatcher::
Submission


submit(Work&&): Result [future]"]:::focal k4(["Application
producer"]):::app k5(["Application
platform-owner work"]):::app - k6["Rendering::Resources::
Provider


create_image(const Assets::Images::Decoded& image):
Rendering::Resources::Image [shared]

upload_geometry(
const Rendering::Geometry::Vertex2D[] vertices
[borrowed],
Rendering::Geometry::Topology topology):
Rendering::Geometry::Buffer2D [shared]"]:::neighbor + k6["Rendering::Resources::
Provider


create_image(const Assets::Images::Decoded&):
Rendering::Resources::Image [shared]

upload_geometry(
const Rendering::Geometry::Vertex2D[] [borrowed],
Rendering::Geometry::Topology):
Rendering::Geometry::Buffer2D [shared]"]:::neighbor k7(["Application
composition"]):::app - k8["Runtime::
GameRuntime


simulation_dispatcher(): Runtime::SimulationDispatcher&"]:::neighbor - k9["Runtime::
SimulationDispatcher


submission(): Runtime::SimulationDispatcher::Submission"]:::focal - k10["Runtime::SimulationDispatcher::
Submission


submit(Work&& work): Result [future]"]:::focal + k8["GameRuntime

simulation_dispatcher(): SimulationDispatcher&"]:::neighbor + k9["SimulationDispatcher

submission(): SimulationDispatcher::Submission"]:::focal + k10["SimulationDispatcher::
Submission


submit(Work&&): Result [future]"]:::focal k11(["Application
simulation-owner work"]):::app k12["Game::
TickContext"]:::neighbor k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/dispatch-internal.mmd b/docs/site/engine-systems/diagrams/dispatch-internal.mmd index e584986f..e55909cf 100644 --- a/docs/site/engine-systems/diagrams/dispatch-internal.mmd +++ b/docs/site/engine-systems/diagrams/dispatch-internal.mmd @@ -13,13 +13,13 @@ config: --- flowchart LR accTitle: Dispatch · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. - k0["Runtime::
Context"]:::neighbor - k1["Runtime::
PlatformDispatcher"]:::focal - k2["Runtime::
GameRuntime"]:::neighbor - k3["Runtime::
SimulationDispatcher"]:::focal - k4["Runtime::PlatformDispatcher::
Submission"]:::focal - k5["Runtime::SimulationDispatcher::
Submission"]:::focal + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + k0["Context"]:::neighbor + k1["PlatformDispatcher"]:::focal + k2["GameRuntime"]:::neighbor + k3["SimulationDispatcher"]:::focal + k4["PlatformDispatcher::
Submission"]:::focal + k5["SimulationDispatcher::
Submission"]:::focal k6["Diagnostics::
DispatchStats"]:::neighbor k7["Text::Rendering::
Updater"]:::neighbor k8["UI::TGUI::
SceneUploader"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/display-application.mmd b/docs/site/engine-systems/diagrams/display-application.mmd index f9c071dd..5ba643da 100644 --- a/docs/site/engine-systems/diagrams/display-application.mmd +++ b/docs/site/engine-systems/diagrams/display-application.mmd @@ -13,20 +13,20 @@ config: --- flowchart LR accTitle: Display · application access - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Platform::; ambiguous nested namespaces remain qualified. Local names omit Platform::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Runtime::
Context


display(): Platform::DisplaySystem&

window(): Platform::Window&

platform_dispatcher(): Runtime::PlatformDispatcher&"]:::neighbor + k1["Runtime::
Context


display(): DisplaySystem&

window(): Window&

platform_dispatcher(): Runtime::PlatformDispatcher&"]:::neighbor k2(["Application
platform-owner work"]):::app - k3["Runtime::PlatformDispatcher::
Submission


submit(Work&& work): Result [future]"]:::neighbor - k4["Platform::
Window


resize(width, height)

set_mode(Platform::WindowMode mode)

hide_cursor(hide)"]:::focal - k5["Platform::
DisplaySystem


monitors(): const Platform::Monitor[]&

primary_monitor(): const Platform::Monitor&

content_scale(const Platform::Monitor& monitor)"]:::focal - k6["Platform::
Monitor"]:::focal - k7["Platform::
Dimensions"]:::focal - k8["Platform::
WindowMode"]:::focal + k3["Runtime::PlatformDispatcher::
Submission


submit(Work&&): Result [future]"]:::neighbor + k4["Window

resize(width, height)

set_mode(WindowMode)

hide_cursor(hide)"]:::focal + k5["DisplaySystem

monitors(): const Monitor[]&

primary_monitor(): const Monitor&

content_scale(const Monitor&)"]:::focal + k6["Monitor"]:::focal + k7["Dimensions"]:::focal + k8["WindowMode"]:::focal k9(["Application
update()"]):::app k10["Game::
TickContext"]:::neighbor - k11["Platform::
FramebufferSize"]:::focal - k12["Rendering::
Camera2D


set_framebuffer_size(Platform::FramebufferSize size)"]:::neighbor + k11["FramebufferSize"]:::focal + k12["Rendering::
Camera2D


set_framebuffer_size(FramebufferSize)"]:::neighbor k0 --> k1 k2 --> k3 k2 --> k4 diff --git a/docs/site/engine-systems/diagrams/display-internal.mmd b/docs/site/engine-systems/diagrams/display-internal.mmd index 7a77cb03..dd53f559 100644 --- a/docs/site/engine-systems/diagrams/display-internal.mmd +++ b/docs/site/engine-systems/diagrams/display-internal.mmd @@ -13,17 +13,17 @@ config: --- flowchart LR accTitle: Display · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Platform::; ambiguous nested namespaces remain qualified. Local names omit Platform::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
Context"]:::neighbor - k1["Platform::
DisplaySystem"]:::focal - k2["Platform::GLFW::
DisplaySystem"]:::focal - k3["Platform::
Window"]:::focal - k4["Platform::GLFW::
Window"]:::focal + k1["DisplaySystem"]:::focal + k2["GLFW::
DisplaySystem"]:::focal + k3["Window"]:::focal + k4["GLFW::
Window"]:::focal k5["Input::
System"]:::neighbor k6["Rendering::OpenGL::
GLFWContext"]:::neighbor - k7["Platform::
Dimensions"]:::focal - k8["Platform::
FramebufferSize"]:::focal - k9["Platform::
WindowResized"]:::focal + k7["Dimensions"]:::focal + k8["FramebufferSize"]:::focal + k9["WindowResized"]:::focal k10["Events::
EventSystem"]:::neighbor k11["Game::
TickContext"]:::neighbor k12["Rendering::
Camera2D"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/events-application.mmd b/docs/site/engine-systems/diagrams/events-application.mmd index c145fb60..23c8b4e6 100644 --- a/docs/site/engine-systems/diagrams/events-application.mmd +++ b/docs/site/engine-systems/diagrams/events-application.mmd @@ -13,19 +13,19 @@ config: --- flowchart LR accTitle: Events · application registration and delivery - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
producer"]):::app - k1["Events::
EventChannel"]:::focal - k2["Events::EventBus::
Publisher


notify(
const Events::EventChannel<Payload>& event,
Value&& payload,
const Events::EventBus::ErrorHandler& errors)"]:::focal + k1["EventChannel"]:::focal + k2["EventBus::
Publisher


notify(
const EventChannel<Payload>&,
Value&&,
const EventBus::ErrorHandler&)"]:::focal k3(["Application
listener setup"]):::app - k4["Runtime::
Context


events(): Events::EventBus&"]:::neighbor - k5["Events::
EventBus


register_listener(
const Events::EventChannel<Payload>& event,
Events::EventChannel<Payload>::Callback callback,
Events::EventBus::Delivery delivery,
Events::EventBus::ErrorHandler errors):
Events::EventBus::Registration

publisher(): Events::EventBus::Publisher

unregister_and_wait(
const Events::EventBus::Registration& registration)"]:::focal - k6["Events::EventBus::
Registration


disconnect()"]:::focal + k4["Runtime::
Context


events(): EventBus&"]:::neighbor + k5["EventBus

register_listener(
const EventChannel<Payload>&,
EventChannel<Payload>::Callback,
EventBus::Delivery,
EventBus::ErrorHandler):
EventBus::Registration

publisher(): EventBus::Publisher

unregister_and_wait(const EventBus::Registration&)"]:::focal + k6["EventBus::
Registration


disconnect()"]:::focal k7(["Application
callback"]):::app - k8("Events::simulation_delivery(
Runtime::SimulationDispatcher::Submission endpoint):
Events::EventBus::Delivery"):::focal + k8("simulation_delivery(
Runtime::SimulationDispatcher::Submission):
EventBus::Delivery"):::focal k9["Runtime::SimulationDispatcher::
Submission"]:::neighbor - k10["Events::EventBus::
Delivery"]:::focal - k11["Events::EventBus::
ErrorHandler"]:::focal + k10["EventBus::
Delivery"]:::focal + k11["EventBus::
ErrorHandler"]:::focal k12(["Application
cleanup / quiesce()"]):::app k0 --> k1 k0 --> k2 diff --git a/docs/site/engine-systems/diagrams/events-focus-delivery.mmd b/docs/site/engine-systems/diagrams/events-focus-delivery.mmd index 3c1ca4ac..08fdbf3e 100644 --- a/docs/site/engine-systems/diagrams/events-focus-delivery.mmd +++ b/docs/site/engine-systems/diagrams/events-focus-delivery.mmd @@ -13,17 +13,17 @@ config: --- flowchart LR accTitle: Events · practical delivery choices - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
listener setup"]):::app - k1["Events::
EventBus"]:::focal - k2["Events::EventBus::
Delivery"]:::focal - k3("Events::
platform_delivery()"):::focal - k4("Events::
simulation_delivery()"):::focal - k5("Events::
worker_delivery()"):::focal + k1["EventBus"]:::focal + k2["EventBus::
Delivery"]:::focal + k3("platform_delivery()"):::focal + k4("simulation_delivery()"):::focal + k5("worker_delivery()"):::focal k6["Runtime::PlatformDispatcher::
Submission"]:::neighbor k7["Runtime::SimulationDispatcher::
Submission"]:::neighbor k8["Workers::
Group"]:::neighbor - k9["Events::EventBus::
ErrorHandler"]:::focal + k9["EventBus::
ErrorHandler"]:::focal k0 --> k1 k1 --> k2 k0 -.-> k3 diff --git a/docs/site/engine-systems/diagrams/events-focus-immediate.mmd b/docs/site/engine-systems/diagrams/events-focus-immediate.mmd index a14e1d3f..3c38b42c 100644 --- a/docs/site/engine-systems/diagrams/events-focus-immediate.mmd +++ b/docs/site/engine-systems/diagrams/events-focus-immediate.mmd @@ -13,12 +13,12 @@ config: --- flowchart LR accTitle: Events · simple immediate notification - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
producer"]):::app - k1["Events::
EventBus"]:::focal - k2["Events::
EventChannel"]:::focal + k1["EventBus"]:::focal + k2["EventChannel"]:::focal k3(["Application
listener setup"]):::app - k4["Events::EventBus::
Registration"]:::focal + k4["EventBus::
Registration"]:::focal k5(["Application
callback"]):::app k6(["Application
cleanup / quiesce()"]):::app k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/events-internal.mmd b/docs/site/engine-systems/diagrams/events-internal.mmd index d228dbd9..23407a8d 100644 --- a/docs/site/engine-systems/diagrams/events-internal.mmd +++ b/docs/site/engine-systems/diagrams/events-internal.mmd @@ -13,23 +13,23 @@ config: --- flowchart LR accTitle: Events · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
Context"]:::neighbor - k1["Events::
EventBus"]:::focal + k1["EventBus"]:::focal k2["Input::
System"]:::neighbor - k3["Events::EventBus::
Publisher"]:::focal + k3["EventBus::
Publisher"]:::focal k4["Platform::GLFW::
Window"]:::neighbor - k5["Events::
EventSystem"]:::focal + k5["EventSystem"]:::focal k6["Runtime::
RuntimeServices"]:::neighbor - k7["Events::
EventChannel"]:::focal - k8["Events::EventBus::
Registration"]:::focal - k9["Events::EventBus::
Delivery"]:::focal - k10["Events::EventBus::
ErrorHandler"]:::focal - k11("Events::
platform_delivery()"):::focal + k7["EventChannel"]:::focal + k8["EventBus::
Registration"]:::focal + k9["EventBus::
Delivery"]:::focal + k10["EventBus::
ErrorHandler"]:::focal + k11("platform_delivery()"):::focal k12["Runtime::PlatformDispatcher::
Submission"]:::neighbor - k13("Events::
simulation_delivery()"):::focal + k13("simulation_delivery()"):::focal k14["Runtime::SimulationDispatcher::
Submission"]:::neighbor - k15("Events::
worker_delivery()"):::focal + k15("worker_delivery()"):::focal k16["Workers::
Group"]:::neighbor k0 --> k1 k2 --> k3 diff --git a/docs/site/engine-systems/diagrams/gui-application.mmd b/docs/site/engine-systems/diagrams/gui-application.mmd index 86381324..345f53d0 100644 --- a/docs/site/engine-systems/diagrams/gui-application.mmd +++ b/docs/site/engine-systems/diagrams/gui-application.mmd @@ -13,20 +13,20 @@ config: --- flowchart TB accTitle: GUI · application-owned sessions and publication - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application-owned UI"]):::app - k1["UI::TGUI::
Session


gui(): tgui::BackendGui&

handle_input(
const Input::Events::Record[] records [borrowed],
pointer_selected)

request_keyboard_focus(
Input::Routing::KeyboardPolicy routing)

record(): UI::TGUI::RecordedScene"]:::focal - k2["UI::RmlUi::
Session


context(): Rml::Context&

handle_input(
const Input::Events::Record[] records [borrowed],
pointer_selected)

request_keyboard_focus(
Input::Routing::KeyboardPolicy routing)

record(): UI::RmlUi::RecordedScene"]:::focal + k1["TGUI::
Session


gui(): tgui::BackendGui&

handle_input(
const Input::Events::Record[] [borrowed],
pointer_selected)

request_keyboard_focus(Input::Routing::KeyboardPolicy)

record(): TGUI::RecordedScene"]:::focal + k2["RmlUi::
Session


context(): Rml::Context&

handle_input(
const Input::Events::Record[] [borrowed],
pointer_selected)

request_keyboard_focus(Input::Routing::KeyboardPolicy)

record(): RmlUi::RecordedScene"]:::focal k3["Input::Events::
Record"]:::neighbor k4["Input::Routing::
KeyboardPolicy"]:::neighbor k5["Text::
Font"]:::neighbor - k6["UI::TGUI::
RecordedScene"]:::focal - k7["UI::RmlUi::
RecordedScene"]:::focal + k6["TGUI::
RecordedScene"]:::focal + k7["RmlUi::
RecordedScene"]:::focal k8(["Application
init_platform()"]):::app - k9["UI::TGUI::
SceneUploader


upload(
Rendering::Resources::Provider& provider,
const UI::TGUI::RecordedScene& recording):
UI::TGUI::Scene

submit(
const Runtime::PlatformDispatcher::Submission&
platform,
UI::TGUI::RecordedScene recording):
UI::TGUI::Scene [future]"]:::focal - k10["UI::RmlUi::
SceneUploader


upload(
Rendering::Resources::Provider& provider,
const UI::RmlUi::RecordedScene& recording):
UI::RmlUi::Scene

submit(
const Runtime::PlatformDispatcher::Submission&
platform,
UI::RmlUi::RecordedScene recording):
UI::RmlUi::Scene [future]"]:::focal - k11["UI::TGUI::
Scene


write(Rendering::FrameWriter& frame)"]:::focal - k12["UI::RmlUi::
Scene


write(Rendering::FrameWriter& frame)"]:::focal + k9["TGUI::
SceneUploader


upload(
Rendering::Resources::Provider&,
const TGUI::RecordedScene&):
TGUI::Scene

submit(
const Runtime::PlatformDispatcher::Submission&,
TGUI::RecordedScene):
TGUI::Scene [future]"]:::focal + k10["RmlUi::
SceneUploader


upload(
Rendering::Resources::Provider&,
const RmlUi::RecordedScene&):
RmlUi::Scene

submit(
const Runtime::PlatformDispatcher::Submission&,
RmlUi::RecordedScene):
RmlUi::Scene [future]"]:::focal + k11["TGUI::
Scene


write(Rendering::FrameWriter&)"]:::focal + k12["RmlUi::
Scene


write(Rendering::FrameWriter&)"]:::focal k13(["Application
prepare_render_frame()"]):::app k14["Rendering::
FrameWriter"]:::neighbor k0 -.-> k1 diff --git a/docs/site/engine-systems/diagrams/gui-focus-rmlui.mmd b/docs/site/engine-systems/diagrams/gui-focus-rmlui.mmd index ff4852de..6ccaa30a 100644 --- a/docs/site/engine-systems/diagrams/gui-focus-rmlui.mmd +++ b/docs/site/engine-systems/diagrams/gui-focus-rmlui.mmd @@ -13,16 +13,16 @@ config: --- flowchart LR accTitle: GUI · RmlUi publication - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application-owned UI"]):::app - k1["UI::RmlUi::
Session"]:::focal + k1["RmlUi::
Session"]:::focal k2["Input::
System"]:::neighbor k3["Text::
FontService"]:::neighbor - k4["UI::RmlUi::
RecordedScene"]:::focal - k5["UI::RmlUi::
SceneUploader"]:::focal + k4["RmlUi::
RecordedScene"]:::focal + k5["RmlUi::
SceneUploader"]:::focal k6["Runtime::PlatformDispatcher::
Submission"]:::neighbor k7["Rendering::Resources::
Provider"]:::neighbor - k8["UI::RmlUi::
Scene"]:::focal + k8["RmlUi::
Scene"]:::focal k9(["Application
prepare_render_frame()"]):::app k10["Rendering::
PassWriter"]:::neighbor k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/gui-focus-tgui.mmd b/docs/site/engine-systems/diagrams/gui-focus-tgui.mmd index e07ca9e0..d7214ec9 100644 --- a/docs/site/engine-systems/diagrams/gui-focus-tgui.mmd +++ b/docs/site/engine-systems/diagrams/gui-focus-tgui.mmd @@ -13,16 +13,16 @@ config: --- flowchart LR accTitle: GUI · TGUI publication - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application-owned UI"]):::app - k1["UI::TGUI::
Session"]:::focal + k1["TGUI::
Session"]:::focal k2["Input::
System"]:::neighbor k3["Text::
FontService"]:::neighbor - k4["UI::TGUI::
RecordedScene"]:::focal - k5["UI::TGUI::
SceneUploader"]:::focal + k4["TGUI::
RecordedScene"]:::focal + k5["TGUI::
SceneUploader"]:::focal k6["Runtime::PlatformDispatcher::
Submission"]:::neighbor k7["Rendering::Resources::
Provider"]:::neighbor - k8["UI::TGUI::
Scene"]:::focal + k8["TGUI::
Scene"]:::focal k9(["Application
prepare_render_frame()"]):::app k10["Rendering::
PassWriter"]:::neighbor k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/gui-internal.mmd b/docs/site/engine-systems/diagrams/gui-internal.mmd index e86a032a..b455c250 100644 --- a/docs/site/engine-systems/diagrams/gui-internal.mmd +++ b/docs/site/engine-systems/diagrams/gui-internal.mmd @@ -13,19 +13,19 @@ config: --- flowchart LR accTitle: GUI · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. - k0["UI::TGUI::
Session"]:::focal + accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + k0["TGUI::
Session"]:::focal k1["Input::
System"]:::neighbor - k2["UI::RmlUi::
Session"]:::focal + k2["RmlUi::
Session"]:::focal k3["Text::
FontService"]:::neighbor - k4["UI::TGUI::
RecordedScene"]:::focal - k5["UI::RmlUi::
RecordedScene"]:::focal - k6["UI::TGUI::
SceneUploader"]:::focal - k7["UI::RmlUi::
SceneUploader"]:::focal + k4["TGUI::
RecordedScene"]:::focal + k5["RmlUi::
RecordedScene"]:::focal + k6["TGUI::
SceneUploader"]:::focal + k7["RmlUi::
SceneUploader"]:::focal k8["Runtime::PlatformDispatcher::
Submission"]:::neighbor k9["Rendering::Resources::
Provider"]:::neighbor - k10["UI::TGUI::
Scene"]:::focal - k11["UI::RmlUi::
Scene"]:::focal + k10["TGUI::
Scene"]:::focal + k11["RmlUi::
Scene"]:::focal k12["Rendering::
PassWriter"]:::neighbor k0 --> k1 k2 --> k1 diff --git a/docs/site/engine-systems/diagrams/input-application.mmd b/docs/site/engine-systems/diagrams/input-application.mmd index a7b377c7..5f155e4f 100644 --- a/docs/site/engine-systems/diagrams/input-application.mmd +++ b/docs/site/engine-systems/diagrams/input-application.mmd @@ -13,21 +13,21 @@ config: --- flowchart LR accTitle: Input · application actions and controllers - accDescr: Local names omit Input::. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app k1["Runtime::
Context


input(): System&"]:::neighbor k2["System

bindings(): Actions::Bindings&

action_contexts(): Actions::ContextSubmission

keyboard_id(): DeviceId"]:::focal - k3["Actions::
Bindings


bind_button(
Control control,
Actions::Id action):
Actions::BindingId

bind_axis(
Control axis,
Actions::Id action,
Actions::AxisOptions options):
Actions::BindingId

unbind(Actions::BindingId binding)"]:::focal + k3["Actions::
Bindings


bind_button(Control, Actions::Id): Actions::BindingId

bind_axis(Control, Actions::Id, Actions::AxisOptions):
Actions::BindingId

unbind(Actions::BindingId)"]:::focal k4["Control"]:::focal k5["KeyboardKey"]:::focal k6(["Application-owned controller"]):::app k7["Actions::
ContextDefinition"]:::focal - k8["Actions::
ContextSubmission


attach(Actions::ContextDefinition definition):
Actions::Context"]:::focal + k8["Actions::
ContextSubmission


attach(Actions::ContextDefinition): Actions::Context"]:::focal k9["Actions::
Context


id(): Actions::ContextId

reset()"]:::focal k10(["Application
update()"]):::app k11["Game::
TickContext"]:::neighbor - k12["Tick

actions(const Actions::Context& context):
Actions::View [borrowed]

button(Actions::Id action):
Actions::ButtonTickState

axis(Actions::Id action):
Actions::AxisTickState"]:::focal - k13["Actions::
View


button(Actions::Id action):
Actions::ButtonTickState

axis(Actions::Id action):
Actions::AxisTickState"]:::focal + k12["Tick

actions(const Actions::Context&):
Actions::View [borrowed]

button(Actions::Id): Actions::ButtonTickState

axis(Actions::Id): Actions::AxisTickState"]:::focal + k13["Actions::
View


button(Actions::Id): Actions::ButtonTickState

axis(Actions::Id): Actions::AxisTickState"]:::focal k2 --> k3 k3 --> k4 k4 --> k5 diff --git a/docs/site/engine-systems/diagrams/input-focus-actions.mmd b/docs/site/engine-systems/diagrams/input-focus-actions.mmd index 262ceb57..3cde4e13 100644 --- a/docs/site/engine-systems/diagrams/input-focus-actions.mmd +++ b/docs/site/engine-systems/diagrams/input-focus-actions.mmd @@ -13,16 +13,16 @@ config: --- flowchart LR accTitle: Input · simple gameplay actions - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Input::
System"]:::focal - k2["Input::Actions::
Bindings"]:::focal - k3["Input::
Control"]:::focal - k4["Input::
KeyboardKey"]:::focal - k5["Input::
PollSnapshot"]:::focal + k1["System"]:::focal + k2["Actions::
Bindings"]:::focal + k3["Control"]:::focal + k4["KeyboardKey"]:::focal + k5["PollSnapshot"]:::focal k6(["Application
update()"]):::app - k7["Input::
Tick"]:::focal - k8["Input::Actions::
ButtonTickState"]:::focal + k7["Tick"]:::focal + k8["Actions::
ButtonTickState"]:::focal k0 --> k1 k1 --> k2 k2 --> k3 diff --git a/docs/site/engine-systems/diagrams/input-focus-capture.mmd b/docs/site/engine-systems/diagrams/input-focus-capture.mmd index da40eb64..cda9392d 100644 --- a/docs/site/engine-systems/diagrams/input-focus-capture.mmd +++ b/docs/site/engine-systems/diagrams/input-focus-capture.mmd @@ -13,13 +13,13 @@ config: --- flowchart LR accTitle: Input · practical UI capture and focus - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application-owned UI"]):::app - k1["Input::
System"]:::focal - k2["Input::Capture::
Lease"]:::focal - k3["Input::Routing::
Manager"]:::focal - k4["Input::Routing::
Lease"]:::focal - k5["Input::Routing::
KeyboardPolicy"]:::focal + k1["System"]:::focal + k2["Capture::
Lease"]:::focal + k3["Routing::
Manager"]:::focal + k4["Routing::
Lease"]:::focal + k5["Routing::
KeyboardPolicy"]:::focal k6["Input::Events::
Record"]:::focal k7["Input::Events::
Payload"]:::focal k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/input-internal.mmd b/docs/site/engine-systems/diagrams/input-internal.mmd index aa20b64c..9b733611 100644 --- a/docs/site/engine-systems/diagrams/input-internal.mmd +++ b/docs/site/engine-systems/diagrams/input-internal.mmd @@ -13,22 +13,22 @@ config: --- flowchart LR accTitle: Input · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
GameRuntime"]:::neighbor - k1["Input::
System"]:::focal + k1["System"]:::focal k2["Platform::GLFW::
InputSystem"]:::focal k3["Platform::
Window"]:::neighbor - k4["Input::Actions::
Bindings"]:::focal - k5["Input::Actions::
ContextSubmission"]:::focal - k6["Input::Actions::
ContextDefinition"]:::focal - k7["Input::Actions::
Context"]:::focal - k8["Input::Capture::
Collector"]:::focal - k9["Input::Routing::
Manager"]:::focal - k10["Input::
PollSnapshot"]:::focal - k11["Input::
PollingBacklog"]:::focal + k4["Actions::
Bindings"]:::focal + k5["Actions::
ContextSubmission"]:::focal + k6["Actions::
ContextDefinition"]:::focal + k7["Actions::
Context"]:::focal + k8["Capture::
Collector"]:::focal + k9["Routing::
Manager"]:::focal + k10["PollSnapshot"]:::focal + k11["PollingBacklog"]:::focal k12["Game::
TickContext"]:::neighbor - k13["Input::
Tick"]:::focal - k14["Input::Actions::
View"]:::focal + k13["Tick"]:::focal + k14["Actions::
View"]:::focal k15["UI::TGUI::
Session"]:::neighbor k16["UI::RmlUi::
Session"]:::neighbor k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/logging-application.mmd b/docs/site/engine-systems/diagrams/logging-application.mmd index 3cc856a0..b369235b 100644 --- a/docs/site/engine-systems/diagrams/logging-application.mmd +++ b/docs/site/engine-systems/diagrams/logging-application.mmd @@ -13,22 +13,22 @@ config: --- flowchart TB accTitle: Logging · application configuration and writers - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Logging::; ambiguous nested namespaces remain qualified. Local names omit Logging::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app - k1["Logging::
ProcessLifetime


ProcessLifetime()"]:::focal - k2["Logging::
Config


for_logger(name, Logging::Profile profile):
Logging::Config"]:::focal - k3["Logging::
Log


Log(
Logging::FileEventHandlers handlers,
Logging::Config configuration)

info(format, arguments...)

close(timeout)"]:::focal - k4["Logging::
Profile"]:::focal - k5["Logging::
Level"]:::focal - k6["Logging::
OverflowPolicy"]:::focal - k7["Logging::
FileEventHandlers"]:::focal + k1["ProcessLifetime

ProcessLifetime()"]:::focal + k2["Config

for_logger(name, Profile): Config"]:::focal + k3["Log

Log(FileEventHandlers, Config)

info(format, arguments...)

close(timeout)"]:::focal + k4["Profile"]:::focal + k5["Level"]:::focal + k6["OverflowPolicy"]:::focal + k7["FileEventHandlers"]:::focal k8(["Application
producer"]):::app - k9["Logging::
Logger


initialize(
Logging::FileEventHandlers handlers,
Logging::Config config):
Logging::Log<name>&

info(fmt, args...)

warn(fmt, args...)"]:::focal + k9["Logger

initialize(FileEventHandlers, Config): Log<name>&

info(fmt, args...)

warn(fmt, args...)"]:::focal k10(["Application
report"]):::app - k11["Logging::
LogHandle


shared_queue_stats(): Logging::QueueStats

backend_stats(): Logging::BackendStats

report_diagnostics()"]:::focal - k12["Logging::
QueueStats"]:::neighbor + k11["LogHandle

shared_queue_stats(): QueueStats

backend_stats(): BackendStats

report_diagnostics()"]:::focal + k12["QueueStats"]:::neighbor k13(["Application
cleanup / quiesce()"]):::app - k14["Logging::
BackendStats"]:::neighbor + k14["BackendStats"]:::neighbor k0 --> k1 k0 --> k2 k0 --> k3 diff --git a/docs/site/engine-systems/diagrams/logging-internal.mmd b/docs/site/engine-systems/diagrams/logging-internal.mmd index 23235f01..67fd6fc0 100644 --- a/docs/site/engine-systems/diagrams/logging-internal.mmd +++ b/docs/site/engine-systems/diagrams/logging-internal.mmd @@ -13,19 +13,19 @@ config: --- flowchart LR accTitle: Logging · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Logging::; ambiguous nested namespaces remain qualified. Local names omit Logging::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
GameRuntime"]:::neighbor - k1["Logging::
Logger"]:::focal + k1["Logger"]:::focal k2["Assets::Loading::
Loader"]:::neighbor k3["Workers::
Pool"]:::neighbor k4["Memory::
Manager"]:::neighbor - k5["Logging::
Log"]:::focal - k6["Logging::
Config"]:::focal - k7["Logging::
LogHandle"]:::focal - k8["Logging::
QueueStats"]:::neighbor - k9["Logging::
FileEventHandlers"]:::focal - k10["Logging::
Level"]:::focal - k11["Logging::
OverflowPolicy"]:::focal + k5["Log"]:::focal + k6["Config"]:::focal + k7["LogHandle"]:::focal + k8["QueueStats"]:::neighbor + k9["FileEventHandlers"]:::focal + k10["Level"]:::focal + k11["OverflowPolicy"]:::focal k0 --> k1 k2 --> k1 k3 --> k1 diff --git a/docs/site/engine-systems/diagrams/memory-application.mmd b/docs/site/engine-systems/diagrams/memory-application.mmd index 073819be..5b7949ec 100644 --- a/docs/site/engine-systems/diagrams/memory-application.mmd +++ b/docs/site/engine-systems/diagrams/memory-application.mmd @@ -13,10 +13,10 @@ config: --- flowchart TB accTitle: Memory · application object storage - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Memory::; ambiguous nested namespaces remain qualified. Local names omit Memory::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
storage owner"]):::app - k1["Memory::Objects::
Pool


get(): Memory::Objects::Pool<T>&

retrieve_objects(N, args...): T[] [shared elements]

cull(age)

release_culled()"]:::focal - k2["Memory::Objects::
Reservation


Reservation(count, Allocator allocator)

emplace(index, args...): T [shared]

remaining_ranges(): const Memory::Block<T>[]& [borrowed]"]:::focal + k1["Objects::
Pool


get(): Objects::Pool<T>&

retrieve_objects(N, args...): T[] [shared elements]

cull(age)

release_culled()"]:::focal + k2["Objects::
Reservation


Reservation(count, Allocator)

emplace(index, args...): T [shared]

remaining_ranges(): const Block<T>[]& [borrowed]"]:::focal k3(["Application
object type"]):::app k4(["Application-owned game objects"]):::app k5(["Application
cleanup / quiesce()"]):::app diff --git a/docs/site/engine-systems/diagrams/memory-internal.mmd b/docs/site/engine-systems/diagrams/memory-internal.mmd index a0f45bae..24061690 100644 --- a/docs/site/engine-systems/diagrams/memory-internal.mmd +++ b/docs/site/engine-systems/diagrams/memory-internal.mmd @@ -13,21 +13,21 @@ config: --- flowchart TB accTitle: Memory · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Memory::; ambiguous nested namespaces remain qualified. Local names omit Memory::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Assets::Cache::
Store"]:::neighbor - k1["Memory::Objects::
Reservation"]:::focal - k2["Memory::Allocators::
ObjectPoolAllocator"]:::focal - k3["Memory::Objects::
Pool"]:::focal + k1["Objects::
Reservation"]:::focal + k2["Allocators::
ObjectPoolAllocator"]:::focal + k3["Objects::
Pool"]:::focal k4("Assets::Grids::
make_geometry()"):::neighbor - k5["Memory::
ExactManager"]:::focal - k6["Memory::
Manager"]:::focal - k7["Memory::Objects::
PoolState"]:::focal - k8["Memory::Objects::Constructor::
Context"]:::focal - k9["Memory::
Block"]:::focal - k10["Memory::
BlockTransactions"]:::focal - k11["Memory::
ByteReleaseContext"]:::focal + k5["ExactManager"]:::focal + k6["Manager"]:::focal + k7["Objects::
PoolState"]:::focal + k8["Objects::Constructor::
Context"]:::focal + k9["Block"]:::focal + k10["BlockTransactions"]:::focal + k11["ByteReleaseContext"]:::focal k12["State::
Singleton"]:::neighbor - k13["Memory::
Stats"]:::neighbor + k13["Stats"]:::neighbor k0 --> k1 k1 --> k2 k2 --> k3 diff --git a/docs/site/engine-systems/diagrams/rendering-application.mmd b/docs/site/engine-systems/diagrams/rendering-application.mmd index 421f0b20..7e555e18 100644 --- a/docs/site/engine-systems/diagrams/rendering-application.mmd +++ b/docs/site/engine-systems/diagrams/rendering-application.mmd @@ -13,17 +13,17 @@ config: --- flowchart LR accTitle: Rendering · application frame preparation - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
update()"]):::app - k1["Rendering::
Camera2D


set_framebuffer_size(Platform::FramebufferSize size)

set_view_matrix(const glm::mat4& view)

projection_matrix(): const glm::mat4&

view_matrix(): const glm::mat4&"]:::focal + k1["Camera2D

set_framebuffer_size(Platform::FramebufferSize)

set_view_matrix(const glm::mat4&)

projection_matrix(): const glm::mat4&

view_matrix(): const glm::mat4&"]:::focal k2(["Application
prepare_render_frame()"]):::app - k3["Rendering::
FrameWriter


begin_pass(
const glm::mat4& projection,
const glm::mat4& view,
Rendering::Pipelines::PassConstraints2D constraints,
Rendering::Parameters::Set parameters):
Rendering::PassWriter [borrowed]"]:::focal - k4["Rendering::
PassWriter


add(Rendering::DrawPacket2D draw)

semantics(): Rendering::Parameters::PassSemantics

parameters(): const Rendering::Parameters::Set&

constraints():
const Rendering::Pipelines::PassConstraints2D&"]:::focal - k5["Rendering::Submission::
Context2D"]:::focal - k6["Rendering::
DrawStyle2D"]:::focal - k7("Rendering::Submission::resolve_sprite(
const Assets::Sprites::Asset& sprite,
Assets::Grids::CellIndex cell,
const Rendering::DrawStyle2D& style,
const Rendering::Submission::Context2D& context):
Rendering::DrawPacket2D"):::focal + k3["FrameWriter

begin_pass(
const glm::mat4&,
const glm::mat4&,
Pipelines::PassConstraints2D,
Parameters::Set):
PassWriter [borrowed]"]:::focal + k4["PassWriter

add(DrawPacket2D)

semantics(): Parameters::PassSemantics

parameters(): const Parameters::Set&

constraints(): const Pipelines::PassConstraints2D&"]:::focal + k5["Submission::
Context2D"]:::focal + k6["DrawStyle2D"]:::focal + k7("Submission::resolve_sprite(
const Assets::Sprites::Asset&,
Assets::Grids::CellIndex,
const DrawStyle2D&,
const Submission::Context2D&):
DrawPacket2D"):::focal k8["Assets::Sprites::
Asset"]:::neighbor - k9["Rendering::
DrawPacket2D"]:::focal + k9["DrawPacket2D"]:::focal k0 --> k1 k2 --> k1 k2 --> k3 diff --git a/docs/site/engine-systems/diagrams/rendering-focus-playback.mmd b/docs/site/engine-systems/diagrams/rendering-focus-playback.mmd index 38147f6f..3caaf572 100644 --- a/docs/site/engine-systems/diagrams/rendering-focus-playback.mmd +++ b/docs/site/engine-systems/diagrams/rendering-focus-playback.mmd @@ -13,13 +13,13 @@ config: --- flowchart LR accTitle: Rendering · simple prepared frame - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. - k0["Rendering::
Renderer"]:::focal - k1["Rendering::
Frame"]:::focal - k2["Rendering::
Pass"]:::focal - k3["Rendering::
DrawPacket2D"]:::focal - k4["Rendering::Resources::
Material"]:::neighbor - k5["Rendering::Geometry::
Buffer2D"]:::neighbor + accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + k0["Renderer"]:::focal + k1["Frame"]:::focal + k2["Pass"]:::focal + k3["DrawPacket2D"]:::focal + k4["Resources::
Material"]:::neighbor + k5["Geometry::
Buffer2D"]:::neighbor k0 --> k1 k1 --> k2 k2 --> k3 diff --git a/docs/site/engine-systems/diagrams/rendering-focus-sprite.mmd b/docs/site/engine-systems/diagrams/rendering-focus-sprite.mmd index eadd31c9..2a8c33ac 100644 --- a/docs/site/engine-systems/diagrams/rendering-focus-sprite.mmd +++ b/docs/site/engine-systems/diagrams/rendering-focus-sprite.mmd @@ -13,16 +13,16 @@ config: --- flowchart LR accTitle: Rendering · practical one-sprite draw - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
prepare_render_frame()"]):::app - k1["Rendering::
FrameWriter"]:::focal - k2["Rendering::
PassWriter"]:::focal - k3["Rendering::
Camera2D"]:::focal - k4("Rendering::Submission::
resolve_sprite()"):::focal - k5["Rendering::Submission::
Context2D"]:::focal + k1["FrameWriter"]:::focal + k2["PassWriter"]:::focal + k3["Camera2D"]:::focal + k4("Submission::
resolve_sprite()"):::focal + k5["Submission::
Context2D"]:::focal k6["Assets::Sprites::
Asset"]:::neighbor - k7["Rendering::
DrawStyle2D"]:::focal - k8["Rendering::
DrawPacket2D"]:::focal + k7["DrawStyle2D"]:::focal + k8["DrawPacket2D"]:::focal k0 --> k1 k1 --> k2 k0 --> k3 diff --git a/docs/site/engine-systems/diagrams/rendering-internal.mmd b/docs/site/engine-systems/diagrams/rendering-internal.mmd index ff4e861c..6ed65061 100644 --- a/docs/site/engine-systems/diagrams/rendering-internal.mmd +++ b/docs/site/engine-systems/diagrams/rendering-internal.mmd @@ -13,23 +13,23 @@ config: --- flowchart TB accTitle: Rendering · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
GameRuntime"]:::neighbor - k1["Rendering::
Renderer"]:::focal - k2["Rendering::
PresentationSurface"]:::focal - k3["Rendering::
Frame"]:::focal - k4["Rendering::OpenGL::
Renderer"]:::focal - k5["Rendering::OpenGL::
GLFWContext"]:::focal - k6["Rendering::OpenGL::
Context"]:::focal - k7["Rendering::
FrameWriter"]:::focal - k8["Rendering::
PassWriter"]:::focal - k9["Rendering::
DrawPacket2D"]:::focal - k10["Rendering::
Pass"]:::focal - k11["Rendering::Resources::
Material"]:::neighbor - k12["Rendering::Geometry::
Buffer2D"]:::neighbor - k13("Rendering::Submission::
resolve_sprite()"):::focal + k1["Renderer"]:::focal + k2["PresentationSurface"]:::focal + k3["Frame"]:::focal + k4["OpenGL::
Renderer"]:::focal + k5["OpenGL::
GLFWContext"]:::focal + k6["OpenGL::
Context"]:::focal + k7["FrameWriter"]:::focal + k8["PassWriter"]:::focal + k9["DrawPacket2D"]:::focal + k10["Pass"]:::focal + k11["Resources::
Material"]:::neighbor + k12["Geometry::
Buffer2D"]:::neighbor + k13("Submission::
resolve_sprite()"):::focal k14["Assets::Sprites::
Asset"]:::neighbor - k15["Rendering::Submission::
Context2D"]:::focal + k15["Submission::
Context2D"]:::focal k0 --> k1 k0 --> k2 k0 --> k3 diff --git a/docs/site/engine-systems/diagrams/resources-application.mmd b/docs/site/engine-systems/diagrams/resources-application.mmd index 129090d5..03b5dccf 100644 --- a/docs/site/engine-systems/diagrams/resources-application.mmd +++ b/docs/site/engine-systems/diagrams/resources-application.mmd @@ -13,18 +13,18 @@ config: --- flowchart TB accTitle: Resources · application uploads and retained draws - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Rendering::Resources::; ambiguous nested namespaces remain qualified. Local names omit Rendering::Resources::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
loading work"]):::app k1("Assets::Images::decode(file): Assets::Images::Decoded"):::neighbor k2["Assets::Images::
Decoded"]:::focal k3(["Application
init_platform()"]):::app - k4["Runtime::
Context


resources(): Rendering::Resources::Provider&"]:::neighbor - k5["Rendering::Resources::
Provider


create_image(const Assets::Images::Decoded& image):
Rendering::Resources::Image [shared]

upload_geometry(
const Rendering::Geometry::Vertex2D[] vertices
[borrowed],
Rendering::Geometry::Topology topology):
Rendering::Geometry::Buffer2D [shared]

graphics_domain(): Rendering::GraphicsDomain"]:::focal + k4["Runtime::
Context


resources(): Provider&"]:::neighbor + k5["Provider

create_image(const Assets::Images::Decoded&):
Image [shared]

upload_geometry(
const Rendering::Geometry::Vertex2D[] [borrowed],
Rendering::Geometry::Topology):
Rendering::Geometry::Buffer2D [shared]

graphics_domain(): Rendering::GraphicsDomain"]:::focal k6(["Application
platform-owner work"]):::app - k7["Rendering::Resources::
Image"]:::focal + k7["Image"]:::focal k8["Rendering::Geometry::
Buffer2D"]:::focal - k9["Rendering::Resources::
Material"]:::focal - k10["Rendering::Resources::
Pipeline"]:::focal + k9["Material"]:::focal + k10["Pipeline"]:::focal k11(["Application
prepare_render_frame()"]):::app k12["Rendering::
DrawPacket2D"]:::neighbor k13(["Application
report"]):::app diff --git a/docs/site/engine-systems/diagrams/resources-internal.mmd b/docs/site/engine-systems/diagrams/resources-internal.mmd index 678eb5f6..93904f59 100644 --- a/docs/site/engine-systems/diagrams/resources-internal.mmd +++ b/docs/site/engine-systems/diagrams/resources-internal.mmd @@ -13,22 +13,22 @@ config: --- flowchart LR accTitle: Resources · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Rendering::Resources::; ambiguous nested namespaces remain qualified. Local names omit Rendering::Resources::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
Context"]:::neighbor - k1["Rendering::Resources::
Provider"]:::focal + k1["Provider"]:::focal k2["Assets::Loading::
Loader"]:::neighbor k3["UI::TGUI::
SceneUploader"]:::neighbor k4["UI::RmlUi::
SceneUploader"]:::neighbor k5("Text::Rendering::
upload()"):::neighbor k6["Rendering::OpenGL::
ResourceProvider"]:::focal - k7["Rendering::Resources::
Image"]:::focal + k7["Image"]:::focal k8["Rendering::Geometry::
Buffer2D"]:::focal - k9["Rendering::Resources::
Program"]:::focal + k9["Program"]:::focal k10["Rendering::
GraphicsDomain"]:::focal k11["Rendering::OpenGL::
ResourceLifetime"]:::focal k12["Rendering::
DrawPacket2D"]:::neighbor - k13["Rendering::Resources::
Material"]:::focal - k14["Rendering::Resources::
Pipeline"]:::focal + k13["Material"]:::focal + k14["Pipeline"]:::focal k15["Rendering::OpenGL::
Pipeline"]:::focal k16["Rendering::OpenGL::
Program"]:::focal k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/runtime-application.mmd b/docs/site/engine-systems/diagrams/runtime-application.mmd index da995434..8fab9113 100644 --- a/docs/site/engine-systems/diagrams/runtime-application.mmd +++ b/docs/site/engine-systems/diagrams/runtime-application.mmd @@ -13,21 +13,21 @@ config: --- flowchart TB accTitle: Runtime · application composition - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app - k1("Application::GLFWOpenGL::make_context(
const Application::GLFWOpenGL::Options& config):
Runtime::Context [unique]"):::focal - k2["Runtime::
Context"]:::focal - k3["Runtime::
GameRuntime


run()

shutdown(): Runtime::ShutdownEndpoint

lifecycle(): Runtime::RuntimeLifecycle

set_shutdown_policy(
Runtime::ShutdownPolicyCallback callback,
Runtime::ShutdownPolicyOptions options,
Events::EventBus::Delivery delivery):
Runtime::ShutdownPolicy"]:::focal + k1("Application::GLFWOpenGL::make_context(
const Application::GLFWOpenGL::Options&):
Context [unique]"):::focal + k2["Context"]:::focal + k3["GameRuntime

run()

shutdown(): ShutdownEndpoint

lifecycle(): RuntimeLifecycle

set_shutdown_policy(
ShutdownPolicyCallback,
ShutdownPolicyOptions,
Events::EventBus::Delivery):
ShutdownPolicy"]:::focal k4(["Application-owned game objects"]):::app - k5["Game::
AbstractGame


Implement hooks
init_platform()

init_simulation()

update(const Game::TickContext& tick)

prepare_render_frame(Rendering::FrameWriter& frame)"]:::focal + k5["Game::
AbstractGame


Implement hooks
init_platform()

init_simulation()

update(const Game::TickContext&)

prepare_render_frame(Rendering::FrameWriter&)"]:::focal k6(["Application
update()"]):::app k7["Game::
TickContext


request_stop()"]:::focal k8(["Application
prepare_render_frame()"]):::app - k9["Rendering::
FrameWriter


begin_pass(
const glm::mat4& projection,
const glm::mat4& view,
Rendering::Pipelines::PassConstraints2D constraints,
Rendering::Parameters::Set parameters):
Rendering::PassWriter [borrowed]"]:::neighbor - k10["Runtime::
ShutdownPolicy


reset()"]:::focal + k9["Rendering::
FrameWriter


begin_pass(
const glm::mat4&,
const glm::mat4&,
Rendering::Pipelines::PassConstraints2D,
Rendering::Parameters::Set):
Rendering::PassWriter [borrowed]"]:::neighbor + k10["ShutdownPolicy

reset()"]:::focal k11(["Application
producer"]):::app - k12["Runtime::
ShutdownEndpoint


request(Runtime::ShutdownReason reason):
Runtime::ShutdownTicket"]:::focal - k13["Runtime::
RuntimeLifecycle


snapshot(): Runtime::RuntimeLifecycleSnapshot"]:::focal + k12["ShutdownEndpoint

request(ShutdownReason): ShutdownTicket"]:::focal + k13["RuntimeLifecycle

snapshot(): RuntimeLifecycleSnapshot"]:::focal k14(["Application
report"]):::app k0 --> k1 k1 --> k2 diff --git a/docs/site/engine-systems/diagrams/runtime-focus-assembly.mmd b/docs/site/engine-systems/diagrams/runtime-focus-assembly.mmd index 9704727a..4a756cc5 100644 --- a/docs/site/engine-systems/diagrams/runtime-focus-assembly.mmd +++ b/docs/site/engine-systems/diagrams/runtime-focus-assembly.mmd @@ -13,9 +13,9 @@ config: --- flowchart LR accTitle: Runtime · simple adapter assembly - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. - k0["Runtime::
GameRuntime"]:::focal - k1["Runtime::
Context"]:::focal + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + k0["GameRuntime"]:::focal + k1["Context"]:::focal k2["Game::
AbstractGame"]:::focal k3["Platform::
DisplaySystem"]:::neighbor k4["Input::
System"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/runtime-focus-frame.mmd b/docs/site/engine-systems/diagrams/runtime-focus-frame.mmd index 9bcb58c9..8db97774 100644 --- a/docs/site/engine-systems/diagrams/runtime-focus-frame.mmd +++ b/docs/site/engine-systems/diagrams/runtime-focus-frame.mmd @@ -13,8 +13,8 @@ config: --- flowchart TB accTitle: Runtime · practical tick and frame boundary - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. - k0["Runtime::
GameRuntime"]:::focal + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + k0["GameRuntime"]:::focal k1["Game::
AbstractGame"]:::focal k2["Game::
TickContext"]:::focal k3["Input::
Tick"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/runtime-focus-shutdown.mmd b/docs/site/engine-systems/diagrams/runtime-focus-shutdown.mmd index c12d43ab..3fed9b77 100644 --- a/docs/site/engine-systems/diagrams/runtime-focus-shutdown.mmd +++ b/docs/site/engine-systems/diagrams/runtime-focus-shutdown.mmd @@ -13,18 +13,18 @@ config: --- flowchart LR accTitle: Runtime · practical shutdown and observation - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
producer"]):::app - k1["Runtime::
ShutdownEndpoint"]:::focal - k2["Runtime::
ShutdownTicket"]:::focal + k1["ShutdownEndpoint"]:::focal + k2["ShutdownTicket"]:::focal k3(["Application
composition"]):::app - k4["Runtime::
ShutdownPolicy"]:::focal - k5["Runtime::
ShutdownDecision"]:::focal - k6["Runtime::
ShutdownRequest"]:::focal - k7["Runtime::
GameRuntime"]:::focal - k8["Runtime::
RuntimeLifecycle"]:::focal + k4["ShutdownPolicy"]:::focal + k5["ShutdownDecision"]:::focal + k6["ShutdownRequest"]:::focal + k7["GameRuntime"]:::focal + k8["RuntimeLifecycle"]:::focal k9(["Application
report"]):::app - k10["Runtime::
RuntimeLifecycleSnapshot"]:::focal + k10["RuntimeLifecycleSnapshot"]:::focal k0 --> k1 k1 --> k2 k3 --> k4 diff --git a/docs/site/engine-systems/diagrams/runtime-internal.mmd b/docs/site/engine-systems/diagrams/runtime-internal.mmd index 1fe312d4..c6c8ced2 100644 --- a/docs/site/engine-systems/diagrams/runtime-internal.mmd +++ b/docs/site/engine-systems/diagrams/runtime-internal.mmd @@ -13,18 +13,18 @@ config: --- flowchart LR accTitle: Runtime · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. - k0["Runtime::
GameRuntime"]:::focal - k1["Runtime::
Context"]:::focal + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + k0["GameRuntime"]:::focal + k1["Context"]:::focal k2["Game::
AbstractGame"]:::focal k3["Rendering::
Frame"]:::neighbor k4["Rendering::
Renderer"]:::neighbor k5["Input::
System"]:::neighbor - k6["Runtime::
RuntimeLifecycle"]:::focal + k6["RuntimeLifecycle"]:::focal k7["Platform::
DisplaySystem"]:::neighbor k8["Rendering::
PresentationSurface"]:::neighbor k9["Rendering::Resources::
Provider"]:::neighbor - k10["Runtime::
PlatformDispatcher"]:::neighbor + k10["PlatformDispatcher"]:::neighbor k11["Workers::
Group"]:::neighbor k12["Events::
EventBus"]:::neighbor k13["Text::
FontService"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/state-application.mmd b/docs/site/engine-systems/diagrams/state-application.mmd index c4cf12af..6388b78c 100644 --- a/docs/site/engine-systems/diagrams/state-application.mmd +++ b/docs/site/engine-systems/diagrams/state-application.mmd @@ -13,14 +13,14 @@ config: --- flowchart TB accTitle: State · application state choices - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit State::; ambiguous nested namespaces remain qualified. Local names omit State::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
state model"]):::app - k1["State::
StateTracker


update(T next)

changed()

delta(): decltype(current() - previous())"]:::focal - k2["State::
StateMachine


add_transition(
State from,
Trigger trigger,
State to,
State::StateMachine<State, Trigger>::Guard guard,
State::StateMachine<State, Trigger>::Action action)

trigger(const Trigger& event)

state(): const State&"]:::focal + k1["StateTracker

update(T)

changed()

delta(): decltype(current() - previous())"]:::focal + k2["StateMachine

add_transition(
State,
Trigger,
State,
StateMachine<State, Trigger>::Guard,
StateMachine<State, Trigger>::Action)

trigger(const Trigger&)

state(): const State&"]:::focal k3(["Application
state producer"]):::app - k4["State::
VersionedVariable


set(T next)

snapshot(): State::VersionedSnapshot<T>

wait_for_change(since): State::VersionedSnapshot<T>"]:::focal - k5["State::
ObservedVariable


ObservedVariable(
T value,
State::ObservedVariable<T, Observers>::Callback[]
observers)

set(T value)

snapshot(): State::VersionedSnapshot<T>"]:::focal - k6["State::
VersionedSnapshot"]:::focal + k4["VersionedVariable

set(T)

snapshot(): VersionedSnapshot<T>

wait_for_change(since): VersionedSnapshot<T>"]:::focal + k5["ObservedVariable

ObservedVariable(
T,
ObservedVariable<T, Observers>::Callback[])

set(T)

snapshot(): VersionedSnapshot<T>"]:::focal + k6["VersionedSnapshot"]:::focal k7(["Application
snapshot reader"]):::app k8(["Application
transition / observer hooks"]):::app k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/state-internal.mmd b/docs/site/engine-systems/diagrams/state-internal.mmd index 062b1c1e..e3ad78aa 100644 --- a/docs/site/engine-systems/diagrams/state-internal.mmd +++ b/docs/site/engine-systems/diagrams/state-internal.mmd @@ -13,16 +13,16 @@ config: --- flowchart LR accTitle: State · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit State::; ambiguous nested namespaces remain qualified. Local names omit State::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Assets::Loading::
Loader"]:::neighbor - k1["State::
Singleton"]:::focal + k1["Singleton"]:::focal k2["Events::
EventSystem"]:::neighbor k3["Memory::Objects::
Pool"]:::neighbor - k4["State::
ObservedVariable"]:::focal - k5["State::
VersionedVariable"]:::focal - k6["State::
VersionedSnapshot"]:::focal - k7["State::
StateTracker"]:::focal - k8["State::
StateMachine"]:::focal + k4["ObservedVariable"]:::focal + k5["VersionedVariable"]:::focal + k6["VersionedSnapshot"]:::focal + k7["StateTracker"]:::focal + k8["StateMachine"]:::focal k0 --> k1 k2 --> k1 k3 --> k1 diff --git a/docs/site/engine-systems/diagrams/text-application.mmd b/docs/site/engine-systems/diagrams/text-application.mmd index cec2869e..53dc80d3 100644 --- a/docs/site/engine-systems/diagrams/text-application.mmd +++ b/docs/site/engine-systems/diagrams/text-application.mmd @@ -13,25 +13,25 @@ config: --- flowchart TB accTitle: Text · application font and label choices - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Runtime::
Context


fonts(): Text::FontService&"]:::neighbor - k2["Text::
FontService


configure(const Text::FontConfiguration& configuration)

default_font(): Text::Font

get(reference): Text::Font"]:::focal + k1["Runtime::
Context


fonts(): FontService&"]:::neighbor + k2["FontService

configure(const FontConfiguration&)

default_font(): Font

get(reference): Font"]:::focal k3(["Application-owned UI"]):::app - k4["Text::
Font


reference()"]:::focal + k4["Font

reference()"]:::focal k5(["Application
loading work"]):::app - k6("Text::layout_text(
const Text::Font& font,
text,
const Text::LayoutOptions& options):
Text::ShapedText"):::focal - k7["Text::
ShapedText


glyphs(): const Text::ShapedGlyph[] [borrowed]

lines(): const Text::TextLine[] [borrowed]"]:::focal - k8("Text::Rendering::prepare(
Text::ShapedText layout,
Text::Rendering::RasterOptions options):
Text::Rendering::Prepared"):::focal + k6("layout_text(const Font&, text, const LayoutOptions&):
ShapedText"):::focal + k7["ShapedText

glyphs(): const ShapedGlyph[] [borrowed]

lines(): const TextLine[] [borrowed]"]:::focal + k8("Text::Rendering::prepare(
ShapedText,
Text::Rendering::RasterOptions):
Text::Rendering::Prepared"):::focal k9["Text::Rendering::
Prepared"]:::focal - k10("Text::Rendering::upload(
Text::Rendering::Prepared prepared,
Rendering::Resources::Provider& provider,
const Rendering::Resources::Material material
[optional shared]):
Text::Rendering::Generation"):::focal + k10("Text::Rendering::upload(
Text::Rendering::Prepared,
Rendering::Resources::Provider&,
const Rendering::Resources::Material [optional
shared]):
Text::Rendering::Generation"):::focal k11["Rendering::Resources::
Provider"]:::neighbor k12["Text::Rendering::
Generation"]:::focal k13(["Application
update()"]):::app - k14["Text::Rendering::
Updater


request(Text::Rendering::Request next)

update()

current(): const Text::Rendering::Generation [shared]"]:::focal + k14["Text::Rendering::
Updater


request(Text::Rendering::Request)

update()

current(): const Text::Rendering::Generation [shared]"]:::focal k15["Text::Rendering::
Request"]:::focal k16(["Application
prepare_render_frame()"]):::app - k17("Rendering::Submission::resolve_text(
const Text::Rendering::Generation& text,
const Rendering::DrawStyle2D& style,
const Rendering::Submission::Context2D& context):
Rendering::DrawPacket2D[]"):::neighbor + k17("Rendering::Submission::resolve_text(
const Text::Rendering::Generation&,
const Rendering::DrawStyle2D&,
const Rendering::Submission::Context2D&):
Rendering::DrawPacket2D[]"):::neighbor k18["Rendering::
DrawPacket2D"]:::neighbor k0 --> k1 k1 --> k2 diff --git a/docs/site/engine-systems/diagrams/text-focus-static.mmd b/docs/site/engine-systems/diagrams/text-focus-static.mmd index f4c72b0a..a1e32df2 100644 --- a/docs/site/engine-systems/diagrams/text-focus-static.mmd +++ b/docs/site/engine-systems/diagrams/text-focus-static.mmd @@ -13,12 +13,12 @@ config: --- flowchart LR accTitle: Text · simple static label - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Text::
FontService"]:::focal - k2["Text::
Font"]:::focal - k3("Text::
layout_text()"):::focal - k4["Text::
ShapedText"]:::focal + k1["FontService"]:::focal + k2["Font"]:::focal + k3("layout_text()"):::focal + k4["ShapedText"]:::focal k5("Text::Rendering::
prepare()"):::focal k6["Text::Rendering::
Prepared"]:::focal k7("Text::Rendering::
upload()"):::focal diff --git a/docs/site/engine-systems/diagrams/text-focus-updater.mmd b/docs/site/engine-systems/diagrams/text-focus-updater.mmd index 8e69210d..ed6c4cbe 100644 --- a/docs/site/engine-systems/diagrams/text-focus-updater.mmd +++ b/docs/site/engine-systems/diagrams/text-focus-updater.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Text · practical changing label - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
update()"]):::app k1["Text::Rendering::
Updater"]:::focal k2["Text::Rendering::
Request"]:::focal diff --git a/docs/site/engine-systems/diagrams/text-internal.mmd b/docs/site/engine-systems/diagrams/text-internal.mmd index e23c5ac4..d6609a69 100644 --- a/docs/site/engine-systems/diagrams/text-internal.mmd +++ b/docs/site/engine-systems/diagrams/text-internal.mmd @@ -13,13 +13,13 @@ config: --- flowchart LR accTitle: Text · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
Context"]:::neighbor - k1["Text::
FontService"]:::focal - k2["Text::
Font"]:::focal - k3["Text::
FontCollection"]:::focal - k4("Text::
layout_text()"):::focal - k5["Text::
ShapedText"]:::focal + k1["FontService"]:::focal + k2["Font"]:::focal + k3["FontCollection"]:::focal + k4("layout_text()"):::focal + k5["ShapedText"]:::focal k6("Text::Rendering::
prepare()"):::focal k7["Text::Rendering::
Prepared"]:::focal k8("Text::Rendering::
upload()"):::focal diff --git a/docs/site/engine-systems/diagrams/utilities-application.mmd b/docs/site/engine-systems/diagrams/utilities-application.mmd index dc46b776..5d55e2c9 100644 --- a/docs/site/engine-systems/diagrams/utilities-application.mmd +++ b/docs/site/engine-systems/diagrams/utilities-application.mmd @@ -15,15 +15,15 @@ flowchart TB accTitle: Utilities · application-local helpers accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
loading tool"]):::app - k1["Files::
FileIndex


FileIndex(const Files::Path& root_path)

get_files_of_type(Files::Extension extension):
const Files::Path[]& [borrowed]

search_directory(const Files::Path& directory)"]:::focal + k1["Files::
FileIndex


FileIndex(const Files::Path&)

get_files_of_type(Files::Extension):
const Files::Path[]& [borrowed]

search_directory(const Files::Path&)"]:::focal k2["Time::
DeltaTime


operator()()

elapsed(): Time::DeltaTime<ClockType>::Duration"]:::focal k3(["Application
bit bookkeeping"]):::app k4["Math::
BitArray


operator[](index): Math::BitArray<Bits>::Reference

words(): Math::BitArray<Bits>::word_type[] [borrowed]"]:::focal k5(["Application
geometry preparation"]):::app - k6["Rendering::
Geometry::Anchor


MakeQuad(
Rendering::Geometry::Pivot pivot,
texture_width,
texture_height,
width,
height,
x0,
y0):
Rendering::Geometry::Quad

MakeQuadStrip(
Rendering::Geometry::Pivot pivot,
texture_width,
texture_height,
width,
height,
x0,
y0):
Rendering::Geometry::QuadTriangleStrip"]:::focal + k6["Rendering::Geometry::
Anchor


MakeQuad(
Rendering::Geometry::Pivot,
texture_width,
texture_height,
width,
height,
x0,
y0):
Rendering::Geometry::Quad

MakeQuadStrip(
Rendering::Geometry::Pivot,
texture_width,
texture_height,
width,
height,
x0,
y0):
Rendering::Geometry::QuadTriangleStrip"]:::focal k7["Rendering::Geometry::
Pivot"]:::focal k8(["Application
storage owner"]):::app - k9("Memory::adjust_length(
length,
Memory::FitPolicy fit,
gb,
gf)"):::focal + k9("Memory::adjust_length(length, Memory::FitPolicy, gb, gf)"):::focal k10["Memory::
FitPolicy"]:::focal k11(["Application
configuration parsing"]):::app k12("Text::Numbers::parse_integers<is_unsigned>(str):
Text::Numbers::NumberVariant"):::focal diff --git a/docs/site/engine-systems/diagrams/workers-application.mmd b/docs/site/engine-systems/diagrams/workers-application.mmd index 3c68e11a..2f65d318 100644 --- a/docs/site/engine-systems/diagrams/workers-application.mmd +++ b/docs/site/engine-systems/diagrams/workers-application.mmd @@ -13,17 +13,17 @@ config: --- flowchart LR accTitle: Workers · application CPU preparation - accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app k1["Runtime::
ExecutionOptions"]:::neighbor - k2["Workers::
Pool


make_group(Workers::GroupOptions options): Workers::Group

shutdown()"]:::focal - k3["Runtime::
Context


make_worker_group(Workers::GroupOptions options):
Workers::Group"]:::neighbor - k4["Workers::
Group


submit(Work&& work): Result [future]

close()

drain()"]:::focal + k2["Pool

make_group(GroupOptions): Group

shutdown()"]:::focal + k3["Runtime::
Context


make_worker_group(GroupOptions): Group"]:::neighbor + k4["Group

submit(Work&&): Result [future]

close()

drain()"]:::focal k5(["Application
CPU preparation"]):::app - k6["Workers::
GroupOptions"]:::focal - k7["Workers::
CpuPolicy"]:::focal + k6["GroupOptions"]:::focal + k7["CpuPolicy"]:::focal k8["Assets::Loading::
Loader


prepare(): Assets::Loading::Prepared"]:::neighbor - k9["Runtime::PlatformDispatcher::
Submission


submit(Work&& work): Result [future]"]:::neighbor + k9["Runtime::PlatformDispatcher::
Submission


submit(Work&&): Result [future]"]:::neighbor k10(["Application
cleanup / quiesce()"]):::app k0 --> k1 k1 --> k2 diff --git a/docs/site/engine-systems/diagrams/workers-focus-groups.mmd b/docs/site/engine-systems/diagrams/workers-focus-groups.mmd index 9cc99a5d..04e7356c 100644 --- a/docs/site/engine-systems/diagrams/workers-focus-groups.mmd +++ b/docs/site/engine-systems/diagrams/workers-focus-groups.mmd @@ -13,14 +13,14 @@ config: --- flowchart LR accTitle: Workers · simple Context-owned groups - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
composition"]):::app k1["Runtime::
ExecutionOptions"]:::neighbor k2["Runtime::
Context"]:::neighbor - k3["Workers::
Pool"]:::focal - k4["Workers::
Group"]:::focal + k3["Pool"]:::focal + k4["Group"]:::focal k5(["Application
CPU preparation"]):::app - k6["Workers::
GroupOptions"]:::focal + k6["GroupOptions"]:::focal k0 --> k1 k2 --> k1 k2 --> k3 diff --git a/docs/site/engine-systems/diagrams/workers-focus-shared.mmd b/docs/site/engine-systems/diagrams/workers-focus-shared.mmd index 80dcbfe8..6192bb05 100644 --- a/docs/site/engine-systems/diagrams/workers-focus-shared.mmd +++ b/docs/site/engine-systems/diagrams/workers-focus-shared.mmd @@ -13,14 +13,14 @@ config: --- flowchart LR accTitle: Workers · practical pool shared by contexts - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
composition"]):::app - k1["Workers::
Pool"]:::focal + k1["Pool"]:::focal k2["Runtime::
ExecutionOptions"]:::neighbor k3["Runtime::
Context
(context A)"]:::neighbor k4["Runtime::
Context
(context B)"]:::neighbor - k5["Workers::
Group
(group A)"]:::focal - k6["Workers::
Group
(group B)"]:::focal + k5["Group"]:::focal + k6["Group"]:::focal k0 --> k1 k0 --> k2 k2 --> k1 diff --git a/docs/site/engine-systems/diagrams/workers-internal.mmd b/docs/site/engine-systems/diagrams/workers-internal.mmd index b2b17921..9b1a5f53 100644 --- a/docs/site/engine-systems/diagrams/workers-internal.mmd +++ b/docs/site/engine-systems/diagrams/workers-internal.mmd @@ -13,18 +13,18 @@ config: --- flowchart TB accTitle: Workers · engine integration - accDescr: Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
Context"]:::neighbor k1["Runtime::
ExecutionOptions"]:::neighbor - k2["Workers::
Pool"]:::focal - k3["Workers::
Group"]:::focal - k4["Workers::
PoolOptions"]:::focal - k5["Workers::
TopologyProvider"]:::focal - k6["Workers::
Topology"]:::focal - k7["Workers::
GroupOptions"]:::focal - k8["Workers::
GroupStatus"]:::neighbor - k9["Workers::
CpuPolicy"]:::focal - k10["Workers::
PolicyStrength"]:::focal + k2["Pool"]:::focal + k3["Group"]:::focal + k4["PoolOptions"]:::focal + k5["TopologyProvider"]:::focal + k6["Topology"]:::focal + k7["GroupOptions"]:::focal + k8["GroupStatus"]:::neighbor + k9["CpuPolicy"]:::focal + k10["PolicyStrength"]:::focal k11["Text::Rendering::
Updater"]:::neighbor k12("Events::
worker_delivery()"):::neighbor k0 --> k1 diff --git a/docs/site/generation/diagram_catalog.py b/docs/site/generation/diagram_catalog.py index 7aad3607..67b24f60 100644 --- a/docs/site/generation/diagram_catalog.py +++ b/docs/site/generation/diagram_catalog.py @@ -20,11 +20,54 @@ def label_text(value: str, *, markup: bool = False) -> str: return re.sub(r"\s+", "", value) -def scoped_label(value: str, namespace: str | None) -> str: +def namespace_collisions(diagram: dict, symbols=None) -> set[str]: + """Keep nested namespaces distinct from the Engine's other root namespaces.""" + namespace = diagram.get("namespace") + return {key.split("::", 1)[0] for key in (symbols if symbols is not None else diagram["nodes"]) + if "::" in key and not key.startswith(str(namespace) + "::")} + + +def scoped_label(value: str, namespace: str | None, collisions=()) -> str: """Elide an explicitly named local namespace without changing type identity.""" if not namespace: return value - return re.sub(r"(? str: + """Keep types and bare placeholders in the curated signatures, omitting typed parameter names.""" + opening = value.find("(") + if opening < 0: + return scoped_label(value, namespace, collisions) + if value[:opening].endswith("operator") and value[opening:opening + 2] == "()": + opening = value.find("(", opening + 2) + if opening < 0: + return scoped_label(value, namespace, collisions) + stack, arguments, start = ["("], [], opening + 1 + pairs = {">": "<", ")": "(", "]": "["} + for index in range(start, len(value)): + char = value[index] + if char in "<([": + stack.append(char) + elif char in pairs: + if not stack or stack.pop() != pairs[char]: + raise ValueError(f"Malformed diagram signature: {value}") + if not stack: + arguments.append(value[start:index].strip()) + break + elif char == "," and stack == ["("]: + arguments.append(value[start:index].strip()) + start = index + 1 + else: + raise ValueError(f"Unclosed diagram signature: {value}") + shortened = [] + for argument in arguments: + match = re.fullmatch(r"(.+?)\s+([A-Za-z_]\w*)(\s+\[[^\]]+\])?", argument) + if match and not re.fullmatch(r"(?:const|volatile|typename)(?:\s+(?:const|volatile|typename))*", match[1]): + argument = match[1] + (match[3] or "") + shortened.append(argument) + return scoped_label(value[:opening + 1] + ", ".join(shortened) + value[index:], namespace, collisions) def frame_version(directory: Path) -> str: @@ -57,6 +100,7 @@ def load_catalog(source: Path) -> dict: raise ValueError(f"Invalid or duplicate Engine diagram ID: {identity}") seen.add(identity) namespace = diagram.get("namespace") + collisions = namespace_collisions(diagram, symbols) if namespace is not None and not re.fullmatch(r"[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*", namespace): raise ValueError(f"Invalid local namespace in Engine diagram {identity}") keys = diagram["nodes"] @@ -98,7 +142,7 @@ def load_catalog(source: Path) -> dict: raise ValueError(f"Invalid operation source in {identity}: {name}") if "CE::" in operation["label"] or "std::" in operation["label"]: raise ValueError(f"Invalid operation display type in {identity}: {name}") - display = scoped_label(operation["label"], namespace) + display = diagram_signature(operation["label"], namespace, collisions) if label_text(display) not in label_text(labels[str(keys.index(key))], markup=True): raise ValueError(f"Engine diagram {identity} omits operation label: {name}") for key, label in diagram.get("operation_labels", {}).items(): @@ -106,7 +150,7 @@ def load_catalog(source: Path) -> dict: raise ValueError(f"Invalid operation box in Engine diagram {identity}: {key}") if "CE::" in label or "std::" in label: raise ValueError(f"Invalid operation display type in {identity}: {key}") - if label_text(scoped_label(label, namespace)) != label_text(labels[str(keys.index(key))], markup=True): + if label_text(diagram_signature(label, namespace, collisions)) != label_text(labels[str(keys.index(key))], markup=True): raise ValueError(f"Engine diagram {identity} operation box label differs: {key}") for symbol in symbols.values(): if symbol["label"].startswith("CE::") or "std::" in symbol["label"]: diff --git a/docs/site/generation/extensions/engine_diagrams.py b/docs/site/generation/extensions/engine_diagrams.py index be2af814..27d313aa 100644 --- a/docs/site/generation/extensions/engine_diagrams.py +++ b/docs/site/generation/extensions/engine_diagrams.py @@ -42,6 +42,8 @@ def run(self): container = nodes.container(classes=["engine-diagram"]) container.extend(super().run()) paragraph = nodes.paragraph(classes=["engine-diagram-caption"]) + if diagram.get("namespace"): + paragraph += nodes.Text("Local names omit " + diagram["namespace"] + ":: where unambiguous. ") paragraph += make_refnode( env.app.builder, env.docname, EXPLORER, identity, nodes.inline(text="Explore this diagram and its other views →"), diff --git a/docs/site/generation/static/engine-diagrams/explorer.mjs b/docs/site/generation/static/engine-diagrams/explorer.mjs index e9ca82d7..9df5e3f3 100644 --- a/docs/site/generation/static/engine-diagrams/explorer.mjs +++ b/docs/site/generation/static/engine-diagrams/explorer.mjs @@ -287,7 +287,7 @@ window.galleryState = { : current.level + ' view'); $('#title').textContent = current.title; $('#question').textContent = current.question + - (current.namespace ? ' Local names omit ' + current.namespace + ':: in the diagram.' : ''); + (current.namespace ? ' Local names omit ' + current.namespace + ':: where unambiguous.' : ''); $('#counts').textContent = current.nodes.length + ' boxes · ' + current.edges.length + ' arrows'; document.querySelectorAll('.nav-button') diff --git a/docs/site/generation/tests/test_engine_diagrams.py b/docs/site/generation/tests/test_engine_diagrams.py index 672b2ebd..6597aedf 100644 --- a/docs/site/generation/tests/test_engine_diagrams.py +++ b/docs/site/generation/tests/test_engine_diagrams.py @@ -1,6 +1,7 @@ """Check the source/catalog contract and explorer publication without an Engine build.""" import ast +from html import escape import json from pathlib import Path import sys @@ -90,7 +91,7 @@ def test_template_arguments_survive_label_markup_normalization(self): method = {**operation(), "label": "notify(Events::EventChannel& event)"} catalog["diagrams"][0]["operations"] = {"Runtime::Context": [method]} source = SOURCE.replace('Runtime::Context"]', - 'Runtime::Context
notify(
Events::EventChannel<Payload>& event)"]') + 'Runtime::Context
notify(
Events::EventChannel<Payload>&)"]') self.write(catalog, source) self.assertEqual(load_catalog(self.source), catalog) self.write(catalog, source.replace("Payload", "OtherPayload")) @@ -123,13 +124,47 @@ def test_local_namespace_preserves_nested_and_external_qualifiers(self): diagram["namespace"] = "Runtime" diagram["operations"] = {"Runtime::Context": [method]} source = SOURCE.replace('Runtime::Context"]', - 'Context
attach(Context next, Input::Actions::Context actions): Context"]') + 'Context
attach(Context, Input::Actions::Context): Context"]') self.write(catalog, source) self.assertEqual(load_catalog(self.source), catalog) self.write(catalog, source.replace("Input::Actions::Context", "Input::Context")) with self.assertRaisesRegex(ValueError, "omits operation label"): load_catalog(self.source) + def test_compact_parameters_preserve_types_markers_and_return_expressions(self): + cases = [ + ("update(T next)", "update(T)"), + ("submit(Work&& work): Result [future]", "submit(Work&&): Result [future]"), + ("upload(const Memory::Block[] blocks [borrowed], count): Memory::Pool&", + "upload(const Memory::Block[] [borrowed], count): Memory::Pool&"), + ("transition(State from, State to, State::Machine::Guard guard)", + "transition(State, State, State::Machine::Guard)"), + ("operator()()", "operator()()"), + ("delta(): decltype(current() - previous())", "delta(): decltype(current() - previous())"), + ("parse(str): Text::Numbers::NumberVariant", "parse(str): Text::Numbers::NumberVariant"), + ] + for full, compact in cases: + with self.subTest(signature=full): + catalog = fixture() + catalog["diagrams"][0]["operations"] = {"Runtime::Context": [{**operation(), "label": full}]} + self.write(catalog, SOURCE.replace('Runtime::Context"]', 'Runtime::Context
' + escape(compact) + '"]')) + self.assertEqual(load_catalog(self.source), catalog) + + def test_scope_preserves_a_nested_namespace_matching_another_engine_namespace(self): + catalog = fixture() + catalog["symbols"]["Rendering::FrameWriter"] = {"key": "Rendering::FrameWriter", "label": "Rendering::FrameWriter"} + diagram = catalog["diagrams"][0] + diagram["namespace"] = "Text" + method = {**operation(), "label": "upload(Text::Rendering::Prepared prepared): Text::Rendering::Generation"} + diagram["operations"] = {"Runtime::Context": [method]} + source = SOURCE.replace('Runtime::Context"]', + 'Runtime::Context
upload(Text::Rendering::Prepared): Text::Rendering::Generation"]') + self.write(catalog, source) + self.assertEqual(load_catalog(self.source), catalog) + self.write(catalog, source.replace("Text::Rendering", "Rendering")) + with self.assertRaisesRegex(ValueError, "omits operation label"): + load_catalog(self.source) + class PublicationTests(unittest.TestCase): def test_local_namespace_keeps_full_inspector_and_api_identity(self): From 2c042f78ac9fcb7531b9d4c7fd54c7e513266807 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Wed, 21 Oct 2026 14:11:32 -0700 Subject: [PATCH 96/98] Shows participating types without declaration qualifiers in diagrams Omits const/volatile, pointers and references, and ownership/completion notes from application diagram signatures. Keeps type identities, collection notation and namespace disambiguation, with full contract details retained in the inspector and API links. Updates diagram conventions and checks operator names, return expressions, collection types and qualifier removal. Removes duplicated namespace prose from Mermaid accessibility descriptions during regeneration. Verification: 20 focused tooling tests, all17 application-source qualifier checks, JavaScript syntax and diff checks pass. Cached documentation build and browser verification follow. --- docs/site/development/documentation.md | 19 +++++++++++-------- docs/site/engine-systems/diagram-explorer.md | 11 +++++------ .../diagrams/assets-application.mmd | 10 +++++----- .../diagrams/assets-focus-loading.mmd | 2 +- .../diagrams/assets-focus-playback.mmd | 2 +- .../diagrams/assets-internal.mmd | 2 +- .../diagrams/audio-application.mmd | 8 ++++---- .../diagrams/audio-internal.mmd | 2 +- .../diagrams/dispatch-application.mmd | 12 ++++++------ .../diagrams/dispatch-internal.mmd | 2 +- .../diagrams/display-application.mmd | 8 ++++---- .../diagrams/display-internal.mmd | 2 +- .../diagrams/events-application.mmd | 8 ++++---- .../diagrams/events-focus-delivery.mmd | 2 +- .../diagrams/events-focus-immediate.mmd | 2 +- .../diagrams/events-internal.mmd | 2 +- .../diagrams/gui-application.mmd | 14 +++++++------- .../diagrams/gui-focus-rmlui.mmd | 2 +- .../diagrams/gui-focus-tgui.mmd | 2 +- .../engine-systems/diagrams/gui-internal.mmd | 2 +- .../diagrams/input-application.mmd | 8 ++++---- .../diagrams/input-focus-actions.mmd | 2 +- .../diagrams/input-focus-capture.mmd | 2 +- .../diagrams/input-internal.mmd | 2 +- .../diagrams/logging-application.mmd | 4 ++-- .../diagrams/logging-internal.mmd | 2 +- .../diagrams/memory-application.mmd | 6 +++--- .../diagrams/memory-internal.mmd | 2 +- .../diagrams/rendering-application.mmd | 10 +++++----- .../diagrams/rendering-focus-playback.mmd | 2 +- .../diagrams/rendering-focus-sprite.mmd | 2 +- .../diagrams/rendering-internal.mmd | 2 +- .../diagrams/resources-application.mmd | 6 +++--- .../diagrams/resources-internal.mmd | 2 +- .../diagrams/runtime-application.mmd | 8 ++++---- .../diagrams/runtime-focus-assembly.mmd | 2 +- .../diagrams/runtime-focus-frame.mmd | 2 +- .../diagrams/runtime-focus-shutdown.mmd | 2 +- .../diagrams/runtime-internal.mmd | 2 +- .../diagrams/state-application.mmd | 4 ++-- .../diagrams/state-internal.mmd | 2 +- .../diagrams/text-application.mmd | 16 ++++++++-------- .../diagrams/text-focus-static.mmd | 2 +- .../diagrams/text-focus-updater.mmd | 2 +- .../engine-systems/diagrams/text-internal.mmd | 2 +- .../diagrams/utilities-application.mmd | 4 ++-- .../diagrams/workers-application.mmd | 6 +++--- .../diagrams/workers-focus-groups.mmd | 2 +- .../diagrams/workers-focus-shared.mmd | 2 +- .../diagrams/workers-internal.mmd | 2 +- docs/site/generation/diagram_catalog.py | 16 +++++++++++++--- .../static/engine-diagrams/explorer.mjs | 2 +- .../generation/tests/test_engine_diagrams.py | 19 ++++++++++++------- 53 files changed, 140 insertions(+), 123 deletions(-) diff --git a/docs/site/development/documentation.md b/docs/site/development/documentation.md index 44792199..688b5b6d 100644 --- a/docs/site/development/documentation.md +++ b/docs/site/development/documentation.md @@ -188,7 +188,8 @@ entry points per type, with value types left as values. Each entry records the qualified declaration name, short display label, source file/line, purpose and `role` (`call` or `override`). Use the declaring base type for inherited methods. Include the corresponding signatures beneath the owning type in Mermaid, -omitting parameter names when their types are shown; label checks reject stale +omitting parameter names when their types are shown and declaration qualifiers; +label checks reject stale metadata. Wrapping markup may change whitespace, but must preserve type spelling, including template arguments. Mark override groups **Implement hooks** to distinguish engine callbacks from application calls. The @@ -199,15 +200,17 @@ focused views. Write labels as `method(arguments): QualifiedResultType`. Include relevant Engine argument and result types, including public aliases and types without their own box. Keep argument names in catalog labels for the inspector, and -omit those names from Mermaid when their types are shown. Preserve the declaration's -direct `const`, pointer and reference qualifiers. Omit defaults, the leading `CE::`, and primitive or -`std::` type spellings. Retain names for scalar and library-only arguments; +omit those names from Mermaid when their types are shown. Preserve `const`, +pointer/reference qualifiers and ownership/completion markers in catalog labels; +omit them from Mermaid so the diagram shows the participating types. Keep `[]` +for collections. Omit defaults, the leading `CE::`, and primitive or `std::` +type spellings. Retain names for scalar and library-only arguments; include template parameter types where they clarify generic arguments. A missing result suffix does not imply `void`: scalar and -library-only results are abridged too. Use `[]` for collections, with short -markers such as `[shared]`, `[unique]`, `[future]` or `[borrowed]` where hiding a -library wrapper would otherwise obscure ownership, asynchronous completion or -borrowing. The API reference remains the complete signature. +library-only results are abridged too. Inspector markers such as `[shared]`, +`[unique]`, `[future]` or `[borrowed]` retain ownership, asynchronous completion +and borrowing details from omitted library wrappers. The API reference remains +the complete signature. A view can declare `namespace`, such as `"Input"`, to omit that prefix from its Mermaid labels. Keep nested namespaces (`Actions::Context`) and other diff --git a/docs/site/engine-systems/diagram-explorer.md b/docs/site/engine-systems/diagram-explorer.md index 7e4aa1e0..ed4844fa 100644 --- a/docs/site/engine-systems/diagram-explorer.md +++ b/docs/site/engine-systems/diagram-explorer.md @@ -13,12 +13,11 @@ search also finds method names. Signatures use `method(arguments): ResultType`, with qualified Engine types and public aliases for arguments and results. Diagram signatures omit parameter -names when their types are shown; the inspector keeps the fuller signatures. -They are abridged: a missing result -suffix can mean a scalar or standard-library result and does not imply `void`. -`[]` marks a collection. Short notes such as `[shared]`, `[unique]`, `[future]` -and `[borrowed]` retain ownership, asynchronous-result and borrowing information -when their library wrappers are omitted. The API reference has the full types. +names when their types are shown, declaration qualifiers such as `const`, +pointers/references, and ownership or completion markers. `[]` marks a collection. +The inspector retains those contract details; the API reference has the full +types. A missing result suffix can mean a scalar or standard-library result +and does not imply `void`. When a view names a local namespace, its prefix is omitted in the diagram: within Input, `Actions::Context` means `Input::Actions::Context`. Other namespaces remain explicit, as do local names that would collide with another diff --git a/docs/site/engine-systems/diagrams/assets-application.mmd b/docs/site/engine-systems/diagrams/assets-application.mmd index f147db77..2ec7c3eb 100644 --- a/docs/site/engine-systems/diagrams/assets-application.mmd +++ b/docs/site/engine-systems/diagrams/assets-application.mmd @@ -13,21 +13,21 @@ config: --- flowchart LR accTitle: Assets · application loading and playback - accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
loading work"]):::app - k1["Loading::
Loader


load_assets(Rendering::Resources::Provider&)

prepare(): Loading::Prepared

upload(
Loading::Prepared,
Rendering::Resources::Provider&,
replace_shaders)"]:::focal + k1["Loading::
Loader


load_assets(Rendering::Resources::Provider)

prepare(): Loading::Prepared

upload(
Loading::Prepared,
Rendering::Resources::Provider,
replace_shaders)"]:::focal k2["Loading::
Prepared"]:::focal k3(["Application
init_platform()"]):::app k4["Rendering::Resources::
Provider"]:::neighbor k5(["Application-owned game objects"]):::app - k6["Sprites::
Manager


get_asset(name_space, name):
Sprites::Manager::Handle [shared]"]:::focal - k7["Tilesets::
Manager


get_asset(key): Tilesets::Manager::Handle [shared]"]:::focal + k6["Sprites::
Manager


get_asset(name_space, name): Sprites::Manager::Handle"]:::focal + k7["Tilesets::
Manager


get_asset(key): Tilesets::Manager::Handle"]:::focal k8["Sprites::
Asset


animation(animation, facing): Sprites::Animation"]:::focal k9["Tilesets::
Asset


tile(cell): Tilesets::Tile

animation(name): Tilesets::Animation

cell_at(Grids::CellIndex, elapsed): Grids::CellIndex"]:::focal k10(["Application
update()"]):::app k11["Sprites::
Animation


advance(elapsed)

cell(): Grids::CellIndex"]:::focal k12(["Application
prepare_render_frame()"]):::app - k13("Rendering::Submission::resolve_sprite(
const Sprites::Asset&,
Grids::CellIndex,
const Rendering::DrawStyle2D&,
const Rendering::Submission::Context2D&):
Rendering::DrawPacket2D"):::neighbor + k13("Rendering::Submission::resolve_sprite(
Sprites::Asset,
Grids::CellIndex,
Rendering::DrawStyle2D,
Rendering::Submission::Context2D):
Rendering::DrawPacket2D"):::neighbor k14["Rendering::
DrawPacket2D"]:::neighbor k0 --> k1 k1 --> k2 diff --git a/docs/site/engine-systems/diagrams/assets-focus-loading.mmd b/docs/site/engine-systems/diagrams/assets-focus-loading.mmd index dbb1f227..75057892 100644 --- a/docs/site/engine-systems/diagrams/assets-focus-loading.mmd +++ b/docs/site/engine-systems/diagrams/assets-focus-loading.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Assets · simple loading and lookup - accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app k1["Loading::
Loader"]:::focal k2["Loading::
Prepared"]:::focal diff --git a/docs/site/engine-systems/diagrams/assets-focus-playback.mmd b/docs/site/engine-systems/diagrams/assets-focus-playback.mmd index d00c6879..07a68464 100644 --- a/docs/site/engine-systems/diagrams/assets-focus-playback.mmd +++ b/docs/site/engine-systems/diagrams/assets-focus-playback.mmd @@ -13,7 +13,7 @@ config: --- flowchart TB accTitle: Assets · practical shared sprite, separate playback - accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application-owned game objects"]):::app k1["Sprites::
Asset"]:::focal k2(["Application
update()"]):::app diff --git a/docs/site/engine-systems/diagrams/assets-internal.mmd b/docs/site/engine-systems/diagrams/assets-internal.mmd index eb9dac07..781ddbb7 100644 --- a/docs/site/engine-systems/diagrams/assets-internal.mmd +++ b/docs/site/engine-systems/diagrams/assets-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Assets · engine integration - accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Assets::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Loading::
Loader"]:::focal k1["Loading::
FileRegistry"]:::focal k2["Files::
FileIndex"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/audio-application.mmd b/docs/site/engine-systems/diagrams/audio-application.mmd index 181fb5a6..5159b067 100644 --- a/docs/site/engine-systems/diagrams/audio-application.mmd +++ b/docs/site/engine-systems/diagrams/audio-application.mmd @@ -13,19 +13,19 @@ config: --- flowchart TB accTitle: Audio · application-owned playback and outcomes - accDescr: Local names omit Audio::; ambiguous nested namespaces remain qualified. Local names omit Audio::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Audio::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app - k1["Miniaudio::
System


open_device(Format, PlaybackNotificationOptions):
Miniaudio::System [unique]

publish_events_to(Events::EventBus::Publisher)

pump_with(const Runtime::RuntimeServices&):
Runtime::ServiceAttachment

publish_notifications()"]:::focal + k1["Miniaudio::
System


open_device(Format, PlaybackNotificationOptions):
Miniaudio::System

publish_events_to(Events::EventBus::Publisher)

pump_with(Runtime::RuntimeServices):
Runtime::ServiceAttachment

publish_notifications()"]:::focal k2(["Application
loading work"]):::app k3("Miniaudio::decode_file(file, Miniaudio::DecodeOptions):
Clip"):::focal k4["Clip

from_samples(Format, samples): Clip"]:::focal k5(["Application
sound controller"]):::app - k6["System

play(const Clip&, PlaybackOptions): Voice [shared]

stream(file, PlaybackOptions): Voice [shared]

maintain()

close()"]:::focal + k6["System

play(Clip, PlaybackOptions): Voice

stream(file, PlaybackOptions): Voice

maintain()

close()"]:::focal k7["PlaybackOptions"]:::focal k8["Voice

snapshot(): VoiceSnapshot

pause()

resume()

stop()"]:::focal k9["VoiceSnapshot"]:::focal k10(["Application
listener setup"]):::app - k11["Events::
EventBus


register_listener(
const Events::EventChannel<PlaybackEnded>&,
Events::EventChannel<PlaybackEnded>::Callback,
Events::EventBus::Delivery,
Events::EventBus::ErrorHandler):
Events::EventBus::Registration

publisher(): Events::EventBus::Publisher"]:::neighbor + k11["Events::
EventBus


register_listener(
Events::EventChannel<PlaybackEnded>,
Events::EventChannel<PlaybackEnded>::Callback,
Events::EventBus::Delivery,
Events::EventBus::ErrorHandler):
Events::EventBus::Registration

publisher(): Events::EventBus::Publisher"]:::neighbor k12["PlaybackEnded"]:::focal k13(["Application
init_platform()"]):::app k14["Runtime::
RuntimeServices"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/audio-internal.mmd b/docs/site/engine-systems/diagrams/audio-internal.mmd index cc8bb682..ba87bdf2 100644 --- a/docs/site/engine-systems/diagrams/audio-internal.mmd +++ b/docs/site/engine-systems/diagrams/audio-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Audio · engine integration - accDescr: Local names omit Audio::; ambiguous nested namespaces remain qualified. Local names omit Audio::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Audio::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Miniaudio::
System"]:::focal k1["System"]:::focal k2["Clip"]:::focal diff --git a/docs/site/engine-systems/diagrams/dispatch-application.mmd b/docs/site/engine-systems/diagrams/dispatch-application.mmd index 5becbe11..be2b0fc6 100644 --- a/docs/site/engine-systems/diagrams/dispatch-application.mmd +++ b/docs/site/engine-systems/diagrams/dispatch-application.mmd @@ -13,18 +13,18 @@ config: --- flowchart LR accTitle: Dispatch · application owner requests - accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Context

platform_dispatcher(): PlatformDispatcher&

resources(): Rendering::Resources::Provider&"]:::neighbor + k1["Context

platform_dispatcher(): PlatformDispatcher

resources(): Rendering::Resources::Provider"]:::neighbor k2["PlatformDispatcher

submission(): PlatformDispatcher::Submission"]:::focal - k3["PlatformDispatcher::
Submission


submit(Work&&): Result [future]"]:::focal + k3["PlatformDispatcher::
Submission


submit(Work): Result"]:::focal k4(["Application
producer"]):::app k5(["Application
platform-owner work"]):::app - k6["Rendering::Resources::
Provider


create_image(const Assets::Images::Decoded&):
Rendering::Resources::Image [shared]

upload_geometry(
const Rendering::Geometry::Vertex2D[] [borrowed],
Rendering::Geometry::Topology):
Rendering::Geometry::Buffer2D [shared]"]:::neighbor + k6["Rendering::Resources::
Provider


create_image(Assets::Images::Decoded):
Rendering::Resources::Image

upload_geometry(
Rendering::Geometry::Vertex2D[],
Rendering::Geometry::Topology):
Rendering::Geometry::Buffer2D"]:::neighbor k7(["Application
composition"]):::app - k8["GameRuntime

simulation_dispatcher(): SimulationDispatcher&"]:::neighbor + k8["GameRuntime

simulation_dispatcher(): SimulationDispatcher"]:::neighbor k9["SimulationDispatcher

submission(): SimulationDispatcher::Submission"]:::focal - k10["SimulationDispatcher::
Submission


submit(Work&&): Result [future]"]:::focal + k10["SimulationDispatcher::
Submission


submit(Work): Result"]:::focal k11(["Application
simulation-owner work"]):::app k12["Game::
TickContext"]:::neighbor k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/dispatch-internal.mmd b/docs/site/engine-systems/diagrams/dispatch-internal.mmd index e55909cf..1c22f795 100644 --- a/docs/site/engine-systems/diagrams/dispatch-internal.mmd +++ b/docs/site/engine-systems/diagrams/dispatch-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Dispatch · engine integration - accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Context"]:::neighbor k1["PlatformDispatcher"]:::focal k2["GameRuntime"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/display-application.mmd b/docs/site/engine-systems/diagrams/display-application.mmd index 5ba643da..e9e141b0 100644 --- a/docs/site/engine-systems/diagrams/display-application.mmd +++ b/docs/site/engine-systems/diagrams/display-application.mmd @@ -13,13 +13,13 @@ config: --- flowchart LR accTitle: Display · application access - accDescr: Local names omit Platform::; ambiguous nested namespaces remain qualified. Local names omit Platform::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Platform::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Runtime::
Context


display(): DisplaySystem&

window(): Window&

platform_dispatcher(): Runtime::PlatformDispatcher&"]:::neighbor + k1["Runtime::
Context


display(): DisplaySystem

window(): Window

platform_dispatcher(): Runtime::PlatformDispatcher"]:::neighbor k2(["Application
platform-owner work"]):::app - k3["Runtime::PlatformDispatcher::
Submission


submit(Work&&): Result [future]"]:::neighbor + k3["Runtime::PlatformDispatcher::
Submission


submit(Work): Result"]:::neighbor k4["Window

resize(width, height)

set_mode(WindowMode)

hide_cursor(hide)"]:::focal - k5["DisplaySystem

monitors(): const Monitor[]&

primary_monitor(): const Monitor&

content_scale(const Monitor&)"]:::focal + k5["DisplaySystem

monitors(): Monitor[]

primary_monitor(): Monitor

content_scale(Monitor)"]:::focal k6["Monitor"]:::focal k7["Dimensions"]:::focal k8["WindowMode"]:::focal diff --git a/docs/site/engine-systems/diagrams/display-internal.mmd b/docs/site/engine-systems/diagrams/display-internal.mmd index dd53f559..6a303171 100644 --- a/docs/site/engine-systems/diagrams/display-internal.mmd +++ b/docs/site/engine-systems/diagrams/display-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Display · engine integration - accDescr: Local names omit Platform::; ambiguous nested namespaces remain qualified. Local names omit Platform::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Platform::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
Context"]:::neighbor k1["DisplaySystem"]:::focal k2["GLFW::
DisplaySystem"]:::focal diff --git a/docs/site/engine-systems/diagrams/events-application.mmd b/docs/site/engine-systems/diagrams/events-application.mmd index 23c8b4e6..1085af70 100644 --- a/docs/site/engine-systems/diagrams/events-application.mmd +++ b/docs/site/engine-systems/diagrams/events-application.mmd @@ -13,13 +13,13 @@ config: --- flowchart LR accTitle: Events · application registration and delivery - accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
producer"]):::app k1["EventChannel"]:::focal - k2["EventBus::
Publisher


notify(
const EventChannel<Payload>&,
Value&&,
const EventBus::ErrorHandler&)"]:::focal + k2["EventBus::
Publisher


notify(
EventChannel<Payload>,
Value,
EventBus::ErrorHandler)"]:::focal k3(["Application
listener setup"]):::app - k4["Runtime::
Context


events(): EventBus&"]:::neighbor - k5["EventBus

register_listener(
const EventChannel<Payload>&,
EventChannel<Payload>::Callback,
EventBus::Delivery,
EventBus::ErrorHandler):
EventBus::Registration

publisher(): EventBus::Publisher

unregister_and_wait(const EventBus::Registration&)"]:::focal + k4["Runtime::
Context


events(): EventBus"]:::neighbor + k5["EventBus

register_listener(
EventChannel<Payload>,
EventChannel<Payload>::Callback,
EventBus::Delivery,
EventBus::ErrorHandler):
EventBus::Registration

publisher(): EventBus::Publisher

unregister_and_wait(EventBus::Registration)"]:::focal k6["EventBus::
Registration


disconnect()"]:::focal k7(["Application
callback"]):::app k8("simulation_delivery(
Runtime::SimulationDispatcher::Submission):
EventBus::Delivery"):::focal diff --git a/docs/site/engine-systems/diagrams/events-focus-delivery.mmd b/docs/site/engine-systems/diagrams/events-focus-delivery.mmd index 08fdbf3e..ee96df3f 100644 --- a/docs/site/engine-systems/diagrams/events-focus-delivery.mmd +++ b/docs/site/engine-systems/diagrams/events-focus-delivery.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Events · practical delivery choices - accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
listener setup"]):::app k1["EventBus"]:::focal k2["EventBus::
Delivery"]:::focal diff --git a/docs/site/engine-systems/diagrams/events-focus-immediate.mmd b/docs/site/engine-systems/diagrams/events-focus-immediate.mmd index 3c38b42c..ffb97811 100644 --- a/docs/site/engine-systems/diagrams/events-focus-immediate.mmd +++ b/docs/site/engine-systems/diagrams/events-focus-immediate.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Events · simple immediate notification - accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
producer"]):::app k1["EventBus"]:::focal k2["EventChannel"]:::focal diff --git a/docs/site/engine-systems/diagrams/events-internal.mmd b/docs/site/engine-systems/diagrams/events-internal.mmd index 23407a8d..9533bb8d 100644 --- a/docs/site/engine-systems/diagrams/events-internal.mmd +++ b/docs/site/engine-systems/diagrams/events-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Events · engine integration - accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Events::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
Context"]:::neighbor k1["EventBus"]:::focal k2["Input::
System"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/gui-application.mmd b/docs/site/engine-systems/diagrams/gui-application.mmd index 345f53d0..06f668d6 100644 --- a/docs/site/engine-systems/diagrams/gui-application.mmd +++ b/docs/site/engine-systems/diagrams/gui-application.mmd @@ -13,20 +13,20 @@ config: --- flowchart TB accTitle: GUI · application-owned sessions and publication - accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application-owned UI"]):::app - k1["TGUI::
Session


gui(): tgui::BackendGui&

handle_input(
const Input::Events::Record[] [borrowed],
pointer_selected)

request_keyboard_focus(Input::Routing::KeyboardPolicy)

record(): TGUI::RecordedScene"]:::focal - k2["RmlUi::
Session


context(): Rml::Context&

handle_input(
const Input::Events::Record[] [borrowed],
pointer_selected)

request_keyboard_focus(Input::Routing::KeyboardPolicy)

record(): RmlUi::RecordedScene"]:::focal + k1["TGUI::
Session


gui(): tgui::BackendGui

handle_input(Input::Events::Record[], pointer_selected)

request_keyboard_focus(Input::Routing::KeyboardPolicy)

record(): TGUI::RecordedScene"]:::focal + k2["RmlUi::
Session


context(): Rml::Context

handle_input(Input::Events::Record[], pointer_selected)

request_keyboard_focus(Input::Routing::KeyboardPolicy)

record(): RmlUi::RecordedScene"]:::focal k3["Input::Events::
Record"]:::neighbor k4["Input::Routing::
KeyboardPolicy"]:::neighbor k5["Text::
Font"]:::neighbor k6["TGUI::
RecordedScene"]:::focal k7["RmlUi::
RecordedScene"]:::focal k8(["Application
init_platform()"]):::app - k9["TGUI::
SceneUploader


upload(
Rendering::Resources::Provider&,
const TGUI::RecordedScene&):
TGUI::Scene

submit(
const Runtime::PlatformDispatcher::Submission&,
TGUI::RecordedScene):
TGUI::Scene [future]"]:::focal - k10["RmlUi::
SceneUploader


upload(
Rendering::Resources::Provider&,
const RmlUi::RecordedScene&):
RmlUi::Scene

submit(
const Runtime::PlatformDispatcher::Submission&,
RmlUi::RecordedScene):
RmlUi::Scene [future]"]:::focal - k11["TGUI::
Scene


write(Rendering::FrameWriter&)"]:::focal - k12["RmlUi::
Scene


write(Rendering::FrameWriter&)"]:::focal + k9["TGUI::
SceneUploader


upload(
Rendering::Resources::Provider,
TGUI::RecordedScene):
TGUI::Scene

submit(
Runtime::PlatformDispatcher::Submission,
TGUI::RecordedScene):
TGUI::Scene"]:::focal + k10["RmlUi::
SceneUploader


upload(
Rendering::Resources::Provider,
RmlUi::RecordedScene):
RmlUi::Scene

submit(
Runtime::PlatformDispatcher::Submission,
RmlUi::RecordedScene):
RmlUi::Scene"]:::focal + k11["TGUI::
Scene


write(Rendering::FrameWriter)"]:::focal + k12["RmlUi::
Scene


write(Rendering::FrameWriter)"]:::focal k13(["Application
prepare_render_frame()"]):::app k14["Rendering::
FrameWriter"]:::neighbor k0 -.-> k1 diff --git a/docs/site/engine-systems/diagrams/gui-focus-rmlui.mmd b/docs/site/engine-systems/diagrams/gui-focus-rmlui.mmd index 6ccaa30a..f2a2b213 100644 --- a/docs/site/engine-systems/diagrams/gui-focus-rmlui.mmd +++ b/docs/site/engine-systems/diagrams/gui-focus-rmlui.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: GUI · RmlUi publication - accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application-owned UI"]):::app k1["RmlUi::
Session"]:::focal k2["Input::
System"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/gui-focus-tgui.mmd b/docs/site/engine-systems/diagrams/gui-focus-tgui.mmd index d7214ec9..20abee22 100644 --- a/docs/site/engine-systems/diagrams/gui-focus-tgui.mmd +++ b/docs/site/engine-systems/diagrams/gui-focus-tgui.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: GUI · TGUI publication - accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application-owned UI"]):::app k1["TGUI::
Session"]:::focal k2["Input::
System"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/gui-internal.mmd b/docs/site/engine-systems/diagrams/gui-internal.mmd index b455c250..1bcb33a2 100644 --- a/docs/site/engine-systems/diagrams/gui-internal.mmd +++ b/docs/site/engine-systems/diagrams/gui-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: GUI · engine integration - accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit UI::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["TGUI::
Session"]:::focal k1["Input::
System"]:::neighbor k2["RmlUi::
Session"]:::focal diff --git a/docs/site/engine-systems/diagrams/input-application.mmd b/docs/site/engine-systems/diagrams/input-application.mmd index 5f155e4f..5c5f3ff2 100644 --- a/docs/site/engine-systems/diagrams/input-application.mmd +++ b/docs/site/engine-systems/diagrams/input-application.mmd @@ -13,10 +13,10 @@ config: --- flowchart LR accTitle: Input · application actions and controllers - accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Runtime::
Context


input(): System&"]:::neighbor - k2["System

bindings(): Actions::Bindings&

action_contexts(): Actions::ContextSubmission

keyboard_id(): DeviceId"]:::focal + k1["Runtime::
Context


input(): System"]:::neighbor + k2["System

bindings(): Actions::Bindings

action_contexts(): Actions::ContextSubmission

keyboard_id(): DeviceId"]:::focal k3["Actions::
Bindings


bind_button(Control, Actions::Id): Actions::BindingId

bind_axis(Control, Actions::Id, Actions::AxisOptions):
Actions::BindingId

unbind(Actions::BindingId)"]:::focal k4["Control"]:::focal k5["KeyboardKey"]:::focal @@ -26,7 +26,7 @@ flowchart LR k9["Actions::
Context


id(): Actions::ContextId

reset()"]:::focal k10(["Application
update()"]):::app k11["Game::
TickContext"]:::neighbor - k12["Tick

actions(const Actions::Context&):
Actions::View [borrowed]

button(Actions::Id): Actions::ButtonTickState

axis(Actions::Id): Actions::AxisTickState"]:::focal + k12["Tick

actions(Actions::Context): Actions::View

button(Actions::Id): Actions::ButtonTickState

axis(Actions::Id): Actions::AxisTickState"]:::focal k13["Actions::
View


button(Actions::Id): Actions::ButtonTickState

axis(Actions::Id): Actions::AxisTickState"]:::focal k2 --> k3 k3 --> k4 diff --git a/docs/site/engine-systems/diagrams/input-focus-actions.mmd b/docs/site/engine-systems/diagrams/input-focus-actions.mmd index 3cde4e13..1fa6a901 100644 --- a/docs/site/engine-systems/diagrams/input-focus-actions.mmd +++ b/docs/site/engine-systems/diagrams/input-focus-actions.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Input · simple gameplay actions - accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app k1["System"]:::focal k2["Actions::
Bindings"]:::focal diff --git a/docs/site/engine-systems/diagrams/input-focus-capture.mmd b/docs/site/engine-systems/diagrams/input-focus-capture.mmd index cda9392d..8be5b230 100644 --- a/docs/site/engine-systems/diagrams/input-focus-capture.mmd +++ b/docs/site/engine-systems/diagrams/input-focus-capture.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Input · practical UI capture and focus - accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application-owned UI"]):::app k1["System"]:::focal k2["Capture::
Lease"]:::focal diff --git a/docs/site/engine-systems/diagrams/input-internal.mmd b/docs/site/engine-systems/diagrams/input-internal.mmd index 9b733611..e98ba029 100644 --- a/docs/site/engine-systems/diagrams/input-internal.mmd +++ b/docs/site/engine-systems/diagrams/input-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Input · engine integration - accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Input::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
GameRuntime"]:::neighbor k1["System"]:::focal k2["Platform::GLFW::
InputSystem"]:::focal diff --git a/docs/site/engine-systems/diagrams/logging-application.mmd b/docs/site/engine-systems/diagrams/logging-application.mmd index b369235b..5ec7b0ff 100644 --- a/docs/site/engine-systems/diagrams/logging-application.mmd +++ b/docs/site/engine-systems/diagrams/logging-application.mmd @@ -13,7 +13,7 @@ config: --- flowchart TB accTitle: Logging · application configuration and writers - accDescr: Local names omit Logging::; ambiguous nested namespaces remain qualified. Local names omit Logging::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Logging::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app k1["ProcessLifetime

ProcessLifetime()"]:::focal k2["Config

for_logger(name, Profile): Config"]:::focal @@ -23,7 +23,7 @@ flowchart TB k6["OverflowPolicy"]:::focal k7["FileEventHandlers"]:::focal k8(["Application
producer"]):::app - k9["Logger

initialize(FileEventHandlers, Config): Log<name>&

info(fmt, args...)

warn(fmt, args...)"]:::focal + k9["Logger

initialize(FileEventHandlers, Config): Log<name>

info(fmt, args...)

warn(fmt, args...)"]:::focal k10(["Application
report"]):::app k11["LogHandle

shared_queue_stats(): QueueStats

backend_stats(): BackendStats

report_diagnostics()"]:::focal k12["QueueStats"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/logging-internal.mmd b/docs/site/engine-systems/diagrams/logging-internal.mmd index 67fd6fc0..d8275854 100644 --- a/docs/site/engine-systems/diagrams/logging-internal.mmd +++ b/docs/site/engine-systems/diagrams/logging-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Logging · engine integration - accDescr: Local names omit Logging::; ambiguous nested namespaces remain qualified. Local names omit Logging::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Logging::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
GameRuntime"]:::neighbor k1["Logger"]:::focal k2["Assets::Loading::
Loader"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/memory-application.mmd b/docs/site/engine-systems/diagrams/memory-application.mmd index 5b7949ec..a6cf5af6 100644 --- a/docs/site/engine-systems/diagrams/memory-application.mmd +++ b/docs/site/engine-systems/diagrams/memory-application.mmd @@ -13,10 +13,10 @@ config: --- flowchart TB accTitle: Memory · application object storage - accDescr: Local names omit Memory::; ambiguous nested namespaces remain qualified. Local names omit Memory::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Memory::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
storage owner"]):::app - k1["Objects::
Pool


get(): Objects::Pool<T>&

retrieve_objects(N, args...): T[] [shared elements]

cull(age)

release_culled()"]:::focal - k2["Objects::
Reservation


Reservation(count, Allocator)

emplace(index, args...): T [shared]

remaining_ranges(): const Block<T>[]& [borrowed]"]:::focal + k1["Objects::
Pool


get(): Objects::Pool<T>

retrieve_objects(N, args...): T[]

cull(age)

release_culled()"]:::focal + k2["Objects::
Reservation


Reservation(count, Allocator)

emplace(index, args...): T

remaining_ranges(): Block<T>[]"]:::focal k3(["Application
object type"]):::app k4(["Application-owned game objects"]):::app k5(["Application
cleanup / quiesce()"]):::app diff --git a/docs/site/engine-systems/diagrams/memory-internal.mmd b/docs/site/engine-systems/diagrams/memory-internal.mmd index 24061690..5d64a019 100644 --- a/docs/site/engine-systems/diagrams/memory-internal.mmd +++ b/docs/site/engine-systems/diagrams/memory-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart TB accTitle: Memory · engine integration - accDescr: Local names omit Memory::; ambiguous nested namespaces remain qualified. Local names omit Memory::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Memory::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Assets::Cache::
Store"]:::neighbor k1["Objects::
Reservation"]:::focal k2["Allocators::
ObjectPoolAllocator"]:::focal diff --git a/docs/site/engine-systems/diagrams/rendering-application.mmd b/docs/site/engine-systems/diagrams/rendering-application.mmd index 7e555e18..c61e90c5 100644 --- a/docs/site/engine-systems/diagrams/rendering-application.mmd +++ b/docs/site/engine-systems/diagrams/rendering-application.mmd @@ -13,15 +13,15 @@ config: --- flowchart LR accTitle: Rendering · application frame preparation - accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
update()"]):::app - k1["Camera2D

set_framebuffer_size(Platform::FramebufferSize)

set_view_matrix(const glm::mat4&)

projection_matrix(): const glm::mat4&

view_matrix(): const glm::mat4&"]:::focal + k1["Camera2D

set_framebuffer_size(Platform::FramebufferSize)

set_view_matrix(glm::mat4)

projection_matrix(): glm::mat4

view_matrix(): glm::mat4"]:::focal k2(["Application
prepare_render_frame()"]):::app - k3["FrameWriter

begin_pass(
const glm::mat4&,
const glm::mat4&,
Pipelines::PassConstraints2D,
Parameters::Set):
PassWriter [borrowed]"]:::focal - k4["PassWriter

add(DrawPacket2D)

semantics(): Parameters::PassSemantics

parameters(): const Parameters::Set&

constraints(): const Pipelines::PassConstraints2D&"]:::focal + k3["FrameWriter

begin_pass(
glm::mat4,
glm::mat4,
Pipelines::PassConstraints2D,
Parameters::Set):
PassWriter"]:::focal + k4["PassWriter

add(DrawPacket2D)

semantics(): Parameters::PassSemantics

parameters(): Parameters::Set

constraints(): Pipelines::PassConstraints2D"]:::focal k5["Submission::
Context2D"]:::focal k6["DrawStyle2D"]:::focal - k7("Submission::resolve_sprite(
const Assets::Sprites::Asset&,
Assets::Grids::CellIndex,
const DrawStyle2D&,
const Submission::Context2D&):
DrawPacket2D"):::focal + k7("Submission::resolve_sprite(
Assets::Sprites::Asset,
Assets::Grids::CellIndex,
DrawStyle2D,
Submission::Context2D):
DrawPacket2D"):::focal k8["Assets::Sprites::
Asset"]:::neighbor k9["DrawPacket2D"]:::focal k0 --> k1 diff --git a/docs/site/engine-systems/diagrams/rendering-focus-playback.mmd b/docs/site/engine-systems/diagrams/rendering-focus-playback.mmd index 3caaf572..eddaf850 100644 --- a/docs/site/engine-systems/diagrams/rendering-focus-playback.mmd +++ b/docs/site/engine-systems/diagrams/rendering-focus-playback.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Rendering · simple prepared frame - accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Renderer"]:::focal k1["Frame"]:::focal k2["Pass"]:::focal diff --git a/docs/site/engine-systems/diagrams/rendering-focus-sprite.mmd b/docs/site/engine-systems/diagrams/rendering-focus-sprite.mmd index 2a8c33ac..5d020e69 100644 --- a/docs/site/engine-systems/diagrams/rendering-focus-sprite.mmd +++ b/docs/site/engine-systems/diagrams/rendering-focus-sprite.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Rendering · practical one-sprite draw - accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
prepare_render_frame()"]):::app k1["FrameWriter"]:::focal k2["PassWriter"]:::focal diff --git a/docs/site/engine-systems/diagrams/rendering-internal.mmd b/docs/site/engine-systems/diagrams/rendering-internal.mmd index 6ed65061..a17d064f 100644 --- a/docs/site/engine-systems/diagrams/rendering-internal.mmd +++ b/docs/site/engine-systems/diagrams/rendering-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart TB accTitle: Rendering · engine integration - accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Rendering::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
GameRuntime"]:::neighbor k1["Renderer"]:::focal k2["PresentationSurface"]:::focal diff --git a/docs/site/engine-systems/diagrams/resources-application.mmd b/docs/site/engine-systems/diagrams/resources-application.mmd index 03b5dccf..6b6a76fd 100644 --- a/docs/site/engine-systems/diagrams/resources-application.mmd +++ b/docs/site/engine-systems/diagrams/resources-application.mmd @@ -13,13 +13,13 @@ config: --- flowchart TB accTitle: Resources · application uploads and retained draws - accDescr: Local names omit Rendering::Resources::; ambiguous nested namespaces remain qualified. Local names omit Rendering::Resources::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Rendering::Resources::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
loading work"]):::app k1("Assets::Images::decode(file): Assets::Images::Decoded"):::neighbor k2["Assets::Images::
Decoded"]:::focal k3(["Application
init_platform()"]):::app - k4["Runtime::
Context


resources(): Provider&"]:::neighbor - k5["Provider

create_image(const Assets::Images::Decoded&):
Image [shared]

upload_geometry(
const Rendering::Geometry::Vertex2D[] [borrowed],
Rendering::Geometry::Topology):
Rendering::Geometry::Buffer2D [shared]

graphics_domain(): Rendering::GraphicsDomain"]:::focal + k4["Runtime::
Context


resources(): Provider"]:::neighbor + k5["Provider

create_image(Assets::Images::Decoded): Image

upload_geometry(
Rendering::Geometry::Vertex2D[],
Rendering::Geometry::Topology):
Rendering::Geometry::Buffer2D

graphics_domain(): Rendering::GraphicsDomain"]:::focal k6(["Application
platform-owner work"]):::app k7["Image"]:::focal k8["Rendering::Geometry::
Buffer2D"]:::focal diff --git a/docs/site/engine-systems/diagrams/resources-internal.mmd b/docs/site/engine-systems/diagrams/resources-internal.mmd index 93904f59..0e172bb9 100644 --- a/docs/site/engine-systems/diagrams/resources-internal.mmd +++ b/docs/site/engine-systems/diagrams/resources-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Resources · engine integration - accDescr: Local names omit Rendering::Resources::; ambiguous nested namespaces remain qualified. Local names omit Rendering::Resources::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Rendering::Resources::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
Context"]:::neighbor k1["Provider"]:::focal k2["Assets::Loading::
Loader"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/runtime-application.mmd b/docs/site/engine-systems/diagrams/runtime-application.mmd index 8fab9113..40367cc0 100644 --- a/docs/site/engine-systems/diagrams/runtime-application.mmd +++ b/docs/site/engine-systems/diagrams/runtime-application.mmd @@ -13,17 +13,17 @@ config: --- flowchart TB accTitle: Runtime · application composition - accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app - k1("Application::GLFWOpenGL::make_context(
const Application::GLFWOpenGL::Options&):
Context [unique]"):::focal + k1("Application::GLFWOpenGL::make_context(
Application::GLFWOpenGL::Options):
Context"):::focal k2["Context"]:::focal k3["GameRuntime

run()

shutdown(): ShutdownEndpoint

lifecycle(): RuntimeLifecycle

set_shutdown_policy(
ShutdownPolicyCallback,
ShutdownPolicyOptions,
Events::EventBus::Delivery):
ShutdownPolicy"]:::focal k4(["Application-owned game objects"]):::app - k5["Game::
AbstractGame


Implement hooks
init_platform()

init_simulation()

update(const Game::TickContext&)

prepare_render_frame(Rendering::FrameWriter&)"]:::focal + k5["Game::
AbstractGame


Implement hooks
init_platform()

init_simulation()

update(Game::TickContext)

prepare_render_frame(Rendering::FrameWriter)"]:::focal k6(["Application
update()"]):::app k7["Game::
TickContext


request_stop()"]:::focal k8(["Application
prepare_render_frame()"]):::app - k9["Rendering::
FrameWriter


begin_pass(
const glm::mat4&,
const glm::mat4&,
Rendering::Pipelines::PassConstraints2D,
Rendering::Parameters::Set):
Rendering::PassWriter [borrowed]"]:::neighbor + k9["Rendering::
FrameWriter


begin_pass(
glm::mat4,
glm::mat4,
Rendering::Pipelines::PassConstraints2D,
Rendering::Parameters::Set):
Rendering::PassWriter"]:::neighbor k10["ShutdownPolicy

reset()"]:::focal k11(["Application
producer"]):::app k12["ShutdownEndpoint

request(ShutdownReason): ShutdownTicket"]:::focal diff --git a/docs/site/engine-systems/diagrams/runtime-focus-assembly.mmd b/docs/site/engine-systems/diagrams/runtime-focus-assembly.mmd index 4a756cc5..defee3a8 100644 --- a/docs/site/engine-systems/diagrams/runtime-focus-assembly.mmd +++ b/docs/site/engine-systems/diagrams/runtime-focus-assembly.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Runtime · simple adapter assembly - accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["GameRuntime"]:::focal k1["Context"]:::focal k2["Game::
AbstractGame"]:::focal diff --git a/docs/site/engine-systems/diagrams/runtime-focus-frame.mmd b/docs/site/engine-systems/diagrams/runtime-focus-frame.mmd index 8db97774..d8c715ad 100644 --- a/docs/site/engine-systems/diagrams/runtime-focus-frame.mmd +++ b/docs/site/engine-systems/diagrams/runtime-focus-frame.mmd @@ -13,7 +13,7 @@ config: --- flowchart TB accTitle: Runtime · practical tick and frame boundary - accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["GameRuntime"]:::focal k1["Game::
AbstractGame"]:::focal k2["Game::
TickContext"]:::focal diff --git a/docs/site/engine-systems/diagrams/runtime-focus-shutdown.mmd b/docs/site/engine-systems/diagrams/runtime-focus-shutdown.mmd index 3fed9b77..3793ee9c 100644 --- a/docs/site/engine-systems/diagrams/runtime-focus-shutdown.mmd +++ b/docs/site/engine-systems/diagrams/runtime-focus-shutdown.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Runtime · practical shutdown and observation - accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
producer"]):::app k1["ShutdownEndpoint"]:::focal k2["ShutdownTicket"]:::focal diff --git a/docs/site/engine-systems/diagrams/runtime-internal.mmd b/docs/site/engine-systems/diagrams/runtime-internal.mmd index c6c8ced2..d00a3e17 100644 --- a/docs/site/engine-systems/diagrams/runtime-internal.mmd +++ b/docs/site/engine-systems/diagrams/runtime-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Runtime · engine integration - accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Runtime::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["GameRuntime"]:::focal k1["Context"]:::focal k2["Game::
AbstractGame"]:::focal diff --git a/docs/site/engine-systems/diagrams/state-application.mmd b/docs/site/engine-systems/diagrams/state-application.mmd index 6388b78c..1b39eb04 100644 --- a/docs/site/engine-systems/diagrams/state-application.mmd +++ b/docs/site/engine-systems/diagrams/state-application.mmd @@ -13,10 +13,10 @@ config: --- flowchart TB accTitle: State · application state choices - accDescr: Local names omit State::; ambiguous nested namespaces remain qualified. Local names omit State::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit State::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
state model"]):::app k1["StateTracker

update(T)

changed()

delta(): decltype(current() - previous())"]:::focal - k2["StateMachine

add_transition(
State,
Trigger,
State,
StateMachine<State, Trigger>::Guard,
StateMachine<State, Trigger>::Action)

trigger(const Trigger&)

state(): const State&"]:::focal + k2["StateMachine

add_transition(
State,
Trigger,
State,
StateMachine<State, Trigger>::Guard,
StateMachine<State, Trigger>::Action)

trigger(Trigger)

state(): State"]:::focal k3(["Application
state producer"]):::app k4["VersionedVariable

set(T)

snapshot(): VersionedSnapshot<T>

wait_for_change(since): VersionedSnapshot<T>"]:::focal k5["ObservedVariable

ObservedVariable(
T,
ObservedVariable<T, Observers>::Callback[])

set(T)

snapshot(): VersionedSnapshot<T>"]:::focal diff --git a/docs/site/engine-systems/diagrams/state-internal.mmd b/docs/site/engine-systems/diagrams/state-internal.mmd index e3ad78aa..435a5aac 100644 --- a/docs/site/engine-systems/diagrams/state-internal.mmd +++ b/docs/site/engine-systems/diagrams/state-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: State · engine integration - accDescr: Local names omit State::; ambiguous nested namespaces remain qualified. Local names omit State::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit State::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Assets::Loading::
Loader"]:::neighbor k1["Singleton"]:::focal k2["Events::
EventSystem"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/text-application.mmd b/docs/site/engine-systems/diagrams/text-application.mmd index 53dc80d3..d105676f 100644 --- a/docs/site/engine-systems/diagrams/text-application.mmd +++ b/docs/site/engine-systems/diagrams/text-application.mmd @@ -13,25 +13,25 @@ config: --- flowchart TB accTitle: Text · application font and label choices - accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app - k1["Runtime::
Context


fonts(): FontService&"]:::neighbor - k2["FontService

configure(const FontConfiguration&)

default_font(): Font

get(reference): Font"]:::focal + k1["Runtime::
Context


fonts(): FontService"]:::neighbor + k2["FontService

configure(FontConfiguration)

default_font(): Font

get(reference): Font"]:::focal k3(["Application-owned UI"]):::app k4["Font

reference()"]:::focal k5(["Application
loading work"]):::app - k6("layout_text(const Font&, text, const LayoutOptions&):
ShapedText"):::focal - k7["ShapedText

glyphs(): const ShapedGlyph[] [borrowed]

lines(): const TextLine[] [borrowed]"]:::focal + k6("layout_text(Font, text, LayoutOptions): ShapedText"):::focal + k7["ShapedText

glyphs(): ShapedGlyph[]

lines(): TextLine[]"]:::focal k8("Text::Rendering::prepare(
ShapedText,
Text::Rendering::RasterOptions):
Text::Rendering::Prepared"):::focal k9["Text::Rendering::
Prepared"]:::focal - k10("Text::Rendering::upload(
Text::Rendering::Prepared,
Rendering::Resources::Provider&,
const Rendering::Resources::Material [optional
shared]):
Text::Rendering::Generation"):::focal + k10("Text::Rendering::upload(
Text::Rendering::Prepared,
Rendering::Resources::Provider,
Rendering::Resources::Material):
Text::Rendering::Generation"):::focal k11["Rendering::Resources::
Provider"]:::neighbor k12["Text::Rendering::
Generation"]:::focal k13(["Application
update()"]):::app - k14["Text::Rendering::
Updater


request(Text::Rendering::Request)

update()

current(): const Text::Rendering::Generation [shared]"]:::focal + k14["Text::Rendering::
Updater


request(Text::Rendering::Request)

update()

current(): Text::Rendering::Generation"]:::focal k15["Text::Rendering::
Request"]:::focal k16(["Application
prepare_render_frame()"]):::app - k17("Rendering::Submission::resolve_text(
const Text::Rendering::Generation&,
const Rendering::DrawStyle2D&,
const Rendering::Submission::Context2D&):
Rendering::DrawPacket2D[]"):::neighbor + k17("Rendering::Submission::resolve_text(
Text::Rendering::Generation,
Rendering::DrawStyle2D,
Rendering::Submission::Context2D):
Rendering::DrawPacket2D[]"):::neighbor k18["Rendering::
DrawPacket2D"]:::neighbor k0 --> k1 k1 --> k2 diff --git a/docs/site/engine-systems/diagrams/text-focus-static.mmd b/docs/site/engine-systems/diagrams/text-focus-static.mmd index a1e32df2..42e99519 100644 --- a/docs/site/engine-systems/diagrams/text-focus-static.mmd +++ b/docs/site/engine-systems/diagrams/text-focus-static.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Text · simple static label - accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
init_platform()"]):::app k1["FontService"]:::focal k2["Font"]:::focal diff --git a/docs/site/engine-systems/diagrams/text-focus-updater.mmd b/docs/site/engine-systems/diagrams/text-focus-updater.mmd index ed6c4cbe..84654f82 100644 --- a/docs/site/engine-systems/diagrams/text-focus-updater.mmd +++ b/docs/site/engine-systems/diagrams/text-focus-updater.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Text · practical changing label - accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
update()"]):::app k1["Text::Rendering::
Updater"]:::focal k2["Text::Rendering::
Request"]:::focal diff --git a/docs/site/engine-systems/diagrams/text-internal.mmd b/docs/site/engine-systems/diagrams/text-internal.mmd index d6609a69..c25e9e98 100644 --- a/docs/site/engine-systems/diagrams/text-internal.mmd +++ b/docs/site/engine-systems/diagrams/text-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Text · engine integration - accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Text::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
Context"]:::neighbor k1["FontService"]:::focal k2["Font"]:::focal diff --git a/docs/site/engine-systems/diagrams/utilities-application.mmd b/docs/site/engine-systems/diagrams/utilities-application.mmd index 5d55e2c9..585a7bd6 100644 --- a/docs/site/engine-systems/diagrams/utilities-application.mmd +++ b/docs/site/engine-systems/diagrams/utilities-application.mmd @@ -15,10 +15,10 @@ flowchart TB accTitle: Utilities · application-local helpers accDescr: Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
loading tool"]):::app - k1["Files::
FileIndex


FileIndex(const Files::Path&)

get_files_of_type(Files::Extension):
const Files::Path[]& [borrowed]

search_directory(const Files::Path&)"]:::focal + k1["Files::
FileIndex


FileIndex(Files::Path)

get_files_of_type(Files::Extension): Files::Path[]

search_directory(Files::Path)"]:::focal k2["Time::
DeltaTime


operator()()

elapsed(): Time::DeltaTime<ClockType>::Duration"]:::focal k3(["Application
bit bookkeeping"]):::app - k4["Math::
BitArray


operator[](index): Math::BitArray<Bits>::Reference

words(): Math::BitArray<Bits>::word_type[] [borrowed]"]:::focal + k4["Math::
BitArray


operator[](index): Math::BitArray<Bits>::Reference

words(): Math::BitArray<Bits>::word_type[]"]:::focal k5(["Application
geometry preparation"]):::app k6["Rendering::Geometry::
Anchor


MakeQuad(
Rendering::Geometry::Pivot,
texture_width,
texture_height,
width,
height,
x0,
y0):
Rendering::Geometry::Quad

MakeQuadStrip(
Rendering::Geometry::Pivot,
texture_width,
texture_height,
width,
height,
x0,
y0):
Rendering::Geometry::QuadTriangleStrip"]:::focal k7["Rendering::Geometry::
Pivot"]:::focal diff --git a/docs/site/engine-systems/diagrams/workers-application.mmd b/docs/site/engine-systems/diagrams/workers-application.mmd index 2f65d318..e7e5ea82 100644 --- a/docs/site/engine-systems/diagrams/workers-application.mmd +++ b/docs/site/engine-systems/diagrams/workers-application.mmd @@ -13,17 +13,17 @@ config: --- flowchart LR accTitle: Workers · application CPU preparation - accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. + accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward the API or type used, not call order or data flow. Methods beneath type names are selected application entry points; Implement hooks marks engine-invoked overrides. Dashed arrows mark optional choices. k0(["Application
composition"]):::app k1["Runtime::
ExecutionOptions"]:::neighbor k2["Pool

make_group(GroupOptions): Group

shutdown()"]:::focal k3["Runtime::
Context


make_worker_group(GroupOptions): Group"]:::neighbor - k4["Group

submit(Work&&): Result [future]

close()

drain()"]:::focal + k4["Group

submit(Work): Result

close()

drain()"]:::focal k5(["Application
CPU preparation"]):::app k6["GroupOptions"]:::focal k7["CpuPolicy"]:::focal k8["Assets::Loading::
Loader


prepare(): Assets::Loading::Prepared"]:::neighbor - k9["Runtime::PlatformDispatcher::
Submission


submit(Work&&): Result [future]"]:::neighbor + k9["Runtime::PlatformDispatcher::
Submission


submit(Work): Result"]:::neighbor k10(["Application
cleanup / quiesce()"]):::app k0 --> k1 k1 --> k2 diff --git a/docs/site/engine-systems/diagrams/workers-focus-groups.mmd b/docs/site/engine-systems/diagrams/workers-focus-groups.mmd index 04e7356c..640abe53 100644 --- a/docs/site/engine-systems/diagrams/workers-focus-groups.mmd +++ b/docs/site/engine-systems/diagrams/workers-focus-groups.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Workers · simple Context-owned groups - accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
composition"]):::app k1["Runtime::
ExecutionOptions"]:::neighbor k2["Runtime::
Context"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/workers-focus-shared.mmd b/docs/site/engine-systems/diagrams/workers-focus-shared.mmd index 6192bb05..a8f27352 100644 --- a/docs/site/engine-systems/diagrams/workers-focus-shared.mmd +++ b/docs/site/engine-systems/diagrams/workers-focus-shared.mmd @@ -13,7 +13,7 @@ config: --- flowchart LR accTitle: Workers · practical pool shared by contexts - accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0(["Application
composition"]):::app k1["Pool"]:::focal k2["Runtime::
ExecutionOptions"]:::neighbor diff --git a/docs/site/engine-systems/diagrams/workers-internal.mmd b/docs/site/engine-systems/diagrams/workers-internal.mmd index 9b1a5f53..55bc36de 100644 --- a/docs/site/engine-systems/diagrams/workers-internal.mmd +++ b/docs/site/engine-systems/diagrams/workers-internal.mmd @@ -13,7 +13,7 @@ config: --- flowchart TB accTitle: Workers · engine integration - accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. + accDescr: Local names omit Workers::; ambiguous nested namespaces remain qualified. Arrows point toward dependencies. Application roles are rounded; named API operations have rounded corners. Dashed arrows mark optional choices. k0["Runtime::
Context"]:::neighbor k1["Runtime::
ExecutionOptions"]:::neighbor k2["Pool"]:::focal diff --git a/docs/site/generation/diagram_catalog.py b/docs/site/generation/diagram_catalog.py index 67b24f60..c408d4e2 100644 --- a/docs/site/generation/diagram_catalog.py +++ b/docs/site/generation/diagram_catalog.py @@ -35,8 +35,15 @@ def scoped_label(value: str, namespace: str | None, collisions=()) -> str: return re.sub(r"(? str: + """Show the participating data type, leaving declaration contracts to the inspector.""" + value = re.sub(r"\s+\[(?:shared|unique|future|borrowed|optional shared|shared elements)\]", "", value) + value = re.sub(r"\b(?:const|volatile)\b\s*", "", value).strip() + return re.sub(r"\s*(?:[*&]\s*)+$", "", value).strip() + + def diagram_signature(value: str, namespace: str | None = None, collisions=()) -> str: - """Keep types and bare placeholders in the curated signatures, omitting typed parameter names.""" + """Show participating types and bare placeholders without declaration qualifiers.""" opening = value.find("(") if opening < 0: return scoped_label(value, namespace, collisions) @@ -66,8 +73,11 @@ def diagram_signature(value: str, namespace: str | None = None, collisions=()) - match = re.fullmatch(r"(.+?)\s+([A-Za-z_]\w*)(\s+\[[^\]]+\])?", argument) if match and not re.fullmatch(r"(?:const|volatile|typename)(?:\s+(?:const|volatile|typename))*", match[1]): argument = match[1] + (match[3] or "") - shortened.append(argument) - return scoped_label(value[:opening + 1] + ", ".join(shortened) + value[index:], namespace, collisions) + shortened.append(diagram_type(argument)) + suffix = value[index + 1:] + if suffix.startswith(": "): + suffix = ": " + diagram_type(suffix[2:]) + return scoped_label(value[:opening + 1] + ", ".join(shortened) + ")" + suffix, namespace, collisions) def frame_version(directory: Path) -> str: diff --git a/docs/site/generation/static/engine-diagrams/explorer.mjs b/docs/site/generation/static/engine-diagrams/explorer.mjs index 9df5e3f3..211ee41b 100644 --- a/docs/site/generation/static/engine-diagrams/explorer.mjs +++ b/docs/site/generation/static/engine-diagrams/explorer.mjs @@ -127,7 +127,7 @@ window.galleryState = { (current.system === 'overview' ? '

Each box is a documented responsibility. Click it to open that subsystem. Click an arrow for supporting types.

' : '
Types in this subsystem
Other Engine contracts
Illustrative application roles

Named free functions appear as operation boxes where they form the actual API boundary. Rounded cream boxes are roles, not Engine types.

' + - (current.view === 'application' ? '

Methods beneath a type name are selected application entry points. “Implement hooks” identifies methods you override for the engine to call. Select the box for method contracts, reference links and Accepts/Returns links to the types shown in this view.

Signatures use method(arguments): ResultType. [] marks collections; bracketed notes retain ownership, future or borrowing information. Scalar and standard-library details are abridged; a missing result suffix does not imply void.

' : '')) + + (current.view === 'application' ? '

Methods beneath a type name are selected application entry points. “Implement hooks” identifies methods you override for the engine to call. Select the box for method contracts, reference links and Accepts/Returns links to the types shown in this view.

Signatures show participating types as method(arguments): ResultType; [] marks collections. Parameter names beside types, declaration qualifiers and ownership details appear in the inspector. Scalar and standard-library details are abridged; a missing result suffix does not imply void.

' : '')) + '

Arrows

Consumer → dependency. No chronological order is implied. Dashed arrows mark optional or alternative choices.

Scope and evidence

These are curated maps. Supporting fields/enums and unrelated dependencies are omitted. The C++ API reference supplies the complete declaration contracts.

All Engine arrows have declaration or implementation evidence. Application wiring is illustrative and follows the subsystem contracts.

' + current.refs.map(sourceLink).join('

') + '

Source revision ' + data.metadata.revision.slice(0, 12) + diff --git a/docs/site/generation/tests/test_engine_diagrams.py b/docs/site/generation/tests/test_engine_diagrams.py index 6597aedf..614089e1 100644 --- a/docs/site/generation/tests/test_engine_diagrams.py +++ b/docs/site/generation/tests/test_engine_diagrams.py @@ -91,7 +91,7 @@ def test_template_arguments_survive_label_markup_normalization(self): method = {**operation(), "label": "notify(Events::EventChannel& event)"} catalog["diagrams"][0]["operations"] = {"Runtime::Context": [method]} source = SOURCE.replace('Runtime::Context"]', - 'Runtime::Context
notify(
Events::EventChannel<Payload>&)"]') + 'Runtime::Context
notify(
Events::EventChannel<Payload>)"]') self.write(catalog, source) self.assertEqual(load_catalog(self.source), catalog) self.write(catalog, source.replace("Payload", "OtherPayload")) @@ -110,7 +110,7 @@ def test_wrapped_signature_keeps_argument_and_result_types(self): method = {**operation(), "label": "events(): Events::EventBus&"} catalog["diagrams"][0]["operations"] = {"Runtime::Context": [method]} source = SOURCE.replace('Runtime::Context"]', - 'Runtime::Context
events():
Events::EventBus&"]') + 'Runtime::Context
events():
Events::EventBus"]') self.write(catalog, source) self.assertEqual(load_catalog(self.source), catalog) self.write(catalog, source.replace("Events::EventBus", "Events::EventChannel")) @@ -131,16 +131,21 @@ def test_local_namespace_preserves_nested_and_external_qualifiers(self): with self.assertRaisesRegex(ValueError, "omits operation label"): load_catalog(self.source) - def test_compact_parameters_preserve_types_markers_and_return_expressions(self): + def test_compact_parameters_preserve_types_collections_and_return_expressions(self): cases = [ ("update(T next)", "update(T)"), - ("submit(Work&& work): Result [future]", "submit(Work&&): Result [future]"), + ("submit(Work&& work): Result [future]", "submit(Work): Result"), ("upload(const Memory::Block[] blocks [borrowed], count): Memory::Pool&", - "upload(const Memory::Block[] [borrowed], count): Memory::Pool&"), + "upload(Memory::Block[], count): Memory::Pool"), + ("configure(const Input::Constraints& constraints): const Input::Constraints* const&", + "configure(Input::Constraints): Input::Constraints"), + ("operator&(const Input::Control& control): Input::Control&", + "operator&(Input::Control): Input::Control"), ("transition(State from, State to, State::Machine::Guard guard)", "transition(State, State, State::Machine::Guard)"), ("operator()()", "operator()()"), ("delta(): decltype(current() - previous())", "delta(): decltype(current() - previous())"), + ("mask(): decltype(current() & previous())", "mask(): decltype(current() & previous())"), ("parse(str): Text::Numbers::NumberVariant", "parse(str): Text::Numbers::NumberVariant"), ] for full, compact in cases: @@ -178,7 +183,7 @@ def test_local_namespace_keeps_full_inspector_and_api_identity(self): diagram["operations"] = {"Runtime::Context": [method]} (source / "catalog.json").write_text(json.dumps(catalog)) (source / "runtime-application.mmd").write_text(SOURCE.replace( - 'Runtime::Context"]', 'Context
context(): Context&"]')) + 'Runtime::Context"]', 'Context
context(): Context"]')) destination = Path(directory) / "html" objects = [("CE::Runtime::Context", "", "class", "api/context", "type", 1), (method["name"], "", "function", "api/context", "method", 1)] @@ -204,7 +209,7 @@ def test_typed_free_operation_stays_specific_to_its_view(self): catalog["diagrams"].append({**fixture()["diagrams"][0], "id": "runtime-internal"}) (source / "catalog.json").write_text(json.dumps(catalog)) (source / "runtime-application.mmd").write_text(SOURCE.replace( - "Runtime::Context", "make_context():
Runtime::Context [unique]")) + "Runtime::Context", "make_context():
Runtime::Context")) (source / "runtime-internal.mmd").write_text(SOURCE.replace("Runtime::Context", "make_context()")) destination = Path(directory) / "html" write_catalog(source, destination, [], lambda name: name + ".html", "https://example.com") From ce3eb0d527eac0172a20cc9d5bdeeb9d8b94d810 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Wed, 21 Oct 2026 17:42:42 -0700 Subject: [PATCH 97/98] Adds inline anchored rectangle overlap math Adds CE::Math::overlaps for raw 2D positions and full dimensions, with independent top-left-origin pivots defaulting to the center. Uses the existing Y-up geometry convention and excludes edge contact and rectangles with nonpositive dimensions. The constexpr noexcept helper is header-only and is exposed through cemath.h. Adds focused unit cases for overlap, containment, separation, contact, empty dimensions and independent anchors, plus API and utility guidance. Verification: C++ syntax-tree parsing and staged whitespace checks pass. Native builds and unit execution were not run; no engine binaries were produced. Documentation regeneration follows this commit so source links can resolve the new public header at the recorded revision. --- docs/site/engine-systems/utilities.md | 1 + docs/site/runtime/utility-contracts.md | 25 ++++++++- projects/engine/include/cheryl/cemath.h | 3 +- .../engine/include/cheryl/math/collision.h | 40 ++++++++++++++ .../engine/tests/unit/src/math/collision.cpp | 55 +++++++++++++++++++ 5 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 projects/engine/include/cheryl/math/collision.h create mode 100644 projects/engine/tests/unit/src/math/collision.cpp diff --git a/docs/site/engine-systems/utilities.md b/docs/site/engine-systems/utilities.md index 6f2a3397..736e71ac 100644 --- a/docs/site/engine-systems/utilities.md +++ b/docs/site/engine-systems/utilities.md @@ -16,6 +16,7 @@ Types below are in `CE` unless another namespace is shown. | --- | | {cpp:class}`Time::DeltaTime ` and duration helpers
Measure local wall-clock intervals and convert duration units. [Timing helpers](../runtime/utility-contracts.md#timing-and-numeric-helpers). | | {cpp:class}`Math::BitArray `
Own packed bits with borrowed proxies/native word spans. [Packed bit representation](../runtime/utility-contracts.md#packed-bits-addresses-and-geometry). | +| `CE::Math::overlaps()`
Test two axis-aligned rectangles from positions, dimensions and optional anchors. [Rectangle overlap](../runtime/utility-contracts.md#test-rectangle-overlap). | | `CE::Text::Numbers` parsing functions
Parse floating/signed/unsigned numeric text with explicit syntax and range failures. [Numeric parsing](../runtime/utility-contracts.md#timing-and-numeric-helpers); [declarations](../../../projects/engine/include/cheryl/math/string-numbers.h). | | {cpp:class}`Files::FileIndex `
Own normalized file-discovery results without automatic refresh. [File index](../assets/file-and-font-discovery.md#file-index). | | `CE::Memory::Pointers` and fit/byte helpers
Calculate addresses/alignment, growth and byte formatting without owning storage. [Address preconditions](../runtime/utility-contracts.md#packed-bits-addresses-and-geometry); [fit arithmetic](../runtime/utility-contracts.md#timing-and-numeric-helpers). | diff --git a/docs/site/runtime/utility-contracts.md b/docs/site/runtime/utility-contracts.md index 7769e639..fe5bbb32 100644 --- a/docs/site/runtime/utility-contracts.md +++ b/docs/site/runtime/utility-contracts.md @@ -4,7 +4,8 @@ These utilities do not choose runtime threads, input devices or graphics policy. Their public headers state their ownership and preconditions; value types still retain or borrow external objects according to the caller's chosen type. `CE::State` owns sampled state and publication, `CE::Time` owns clock/duration -helpers, and `CE::Math` owns mathematical representations such as packed bits. +helpers, and `CE::Math` owns rectangle overlap tests and mathematical +representations such as packed bits. Allocation policy and address operations belong to `CE::Memory`; numeric text parsing belongs to `CE::Text::Numbers`, and drawable geometry to `CE::Rendering::Geometry`. Header paths remain stable across these logical owners. @@ -22,6 +23,7 @@ individual overload contracts. | Parse a number or size an allocation | [Parse numbers](#parse-numbers), [size CPU allocations](#size-cpu-allocations) | [Numeric parsing](../../../projects/engine/include/cheryl/math/string-numbers.h), [fit arithmetic](../../../projects/engine/include/cheryl/math/fit.h), [byte formatting](../../../projects/engine/include/cheryl/math/bytes.h). | | Work with packed bits or addresses | [Packed bits, addresses and geometry](#packed-bits-addresses-and-geometry) | [BitArray](../../../projects/engine/include/cheryl/math/binary.h), [pointer helpers](../../../projects/engine/include/cheryl/math/pointers.h). | | Build an anchored quad | [Build geometry](#build-geometry) | [Anchor](../../../projects/engine/include/cheryl/math/anchor.h). | +| Test rectangle collisions | [Test rectangle overlap](#test-rectangle-overlap) | [Overlap helper](../../../projects/engine/include/cheryl/math/collision.h). | | Discover source files | [File discovery](#file-discovery) | [FileIndex](../../../projects/engine/include/cheryl/core/resources/fileio/file-mgr.h). | ## Choose a state representation @@ -172,6 +174,27 @@ Case-sensitive two-letter anchor names include CL/ML and CR/MR aliases; unknown names/enum values select center. The [asset coordinate contract](../assets/asset-values-and-playback.md#coordinates-and-grid-geometry) defines grid geometry and normalized UV ownership. +### Test rectangle overlap + +Include `` or `` and call +`CE::Math::overlaps(p1, p2, d1, d2)` with `glm::vec2` positions and full width/height +vectors in the same Y-up coordinates and units. This inline `constexpr` helper +returns a `bool` without creating engine resources: + +```cpp +const bool colliding = CE::Math::overlaps({0, 0}, {3, 0}, {4, 4}, {4, 4}); // true +``` + +Each position identifies its rectangle's anchor. Optional fifth and sixth +`CE::Rendering::Geometry::Pivot` arguments select independent anchors, both +defaulting to `{0.5f, 0.5f}`. Anchors use the same normalized top-left origin as +the geometry builders: `{0, 0}` places the rectangle to the right of and below its +position in Y-up coordinates. The {cpp:func}`CE::Math::overlaps` reference +owns the exact input preconditions and boundary rules. Touching edges/corners and +nonpositive dimensions return `false`; containment returns `true`. Supply finite +values and representable edges, and apply any scaling or coordinate conversion +before calling. This tests axis-aligned bounds; rotated shapes need another test. + ### Retain blocks and format values Block range operations are caller-synchronized; manager transactions and retained diff --git a/projects/engine/include/cheryl/cemath.h b/projects/engine/include/cheryl/cemath.h index cf7bd0f5..a3848d8c 100644 --- a/projects/engine/include/cheryl/cemath.h +++ b/projects/engine/include/cheryl/cemath.h @@ -1,12 +1,13 @@ #pragma once /** @file - * @brief CPU geometry pivots, packed bits, address arithmetic, durations and capacity-growth helpers. + * @brief CPU geometry pivots, rectangle overlap, packed bits, address arithmetic, durations and capacity-growth helpers. * These helpers do not create a renderer or acquire resource ownership. * @see [Detailed guide](cheryl-guide:runtime/utility-contracts) */ #include "math/anchor.h" #include "math/binary.h" +#include "math/collision.h" #include "math/pointers.h" #include "math/time.h" #include "math/fit.h" diff --git a/projects/engine/include/cheryl/math/collision.h b/projects/engine/include/cheryl/math/collision.h new file mode 100644 index 00000000..5d09b3fa --- /dev/null +++ b/projects/engine/include/cheryl/math/collision.h @@ -0,0 +1,40 @@ +#pragma once + +#include "anchor.h" +#include + +namespace CE::Math { + /** Test whether two axis-aligned rectangles share positive area in Y-up coordinates. + * @param p1 Position of the first rectangle's anchor, in caller-defined units. + * @param p2 Position of the second rectangle's anchor, in the same coordinates and units. + * @param d1 Full width and height of the first rectangle; nonpositive dimensions return false. + * @param d2 Full width and height of the second rectangle; nonpositive dimensions return false. + * @param a1 Normalized top-left-origin anchor of the first rectangle; defaults to {0.5,0.5} (center). + * @param a2 Normalized top-left-origin anchor of the second rectangle; defaults to {0.5,0.5} (center). + * @return True for overlap or containment; false for separation, touching edges/corners or empty rectangles. + * @pre Positions and dimensions are finite, anchors are finite and in [0,1], and calculated edges are representable. + * Rotation, scaling and coordinate conversion are the caller's responsibility. No allocation or renderer is required. + * @see [Rectangle overlap](cheryl-guide:runtime/utility-contracts#test-rectangle-overlap) + */ + [[nodiscard]] constexpr bool overlaps( + const glm::vec2 p1, + const glm::vec2 p2, + const glm::vec2 d1, + const glm::vec2 d2, + const ::CE::Rendering::Geometry::Pivot a1 = ::CE::Rendering::Geometry::Pivot{}, + const ::CE::Rendering::Geometry::Pivot a2 = ::CE::Rendering::Geometry::Pivot{} + ) noexcept { + if (d1.x <= 0.0f || d1.y <= 0.0f || d2.x <= 0.0f || d2.y <= 0.0f) + return false; + + const auto left1 = p1.x - a1.x * d1.x; + const auto right1 = p1.x + (1.0f - a1.x) * d1.x; + const auto bottom1 = p1.y - (1.0f - a1.y) * d1.y; + const auto top1 = p1.y + a1.y * d1.y; + const auto left2 = p2.x - a2.x * d2.x; + const auto right2 = p2.x + (1.0f - a2.x) * d2.x; + const auto bottom2 = p2.y - (1.0f - a2.y) * d2.y; + const auto top2 = p2.y + a2.y * d2.y; + return left1 < right2 && left2 < right1 && bottom1 < top2 && bottom2 < top1; + } +} diff --git a/projects/engine/tests/unit/src/math/collision.cpp b/projects/engine/tests/unit/src/math/collision.cpp new file mode 100644 index 00000000..91acfcd1 --- /dev/null +++ b/projects/engine/tests/unit/src/math/collision.cpp @@ -0,0 +1,55 @@ +#include +#include + +TEST(math_collision, overlap) { + constexpr glm::vec2 position{0.0f, 0.0f}; + constexpr glm::vec2 dimensions{4.0f, 4.0f}; + static_assert(CE::Math::overlaps(position, position, dimensions, dimensions)); + static_assert(noexcept(CE::Math::overlaps(position, position, dimensions, dimensions))); + + for (const auto other : {glm::vec2{0, 0}, glm::vec2{1, 1}, glm::vec2{-2, -2}}) { + EXPECT_TRUE(CE::Math::overlaps(position, other, dimensions, dimensions)); + EXPECT_TRUE(CE::Math::overlaps(other, position, dimensions, dimensions)); + } + EXPECT_TRUE(CE::Math::overlaps(position, position, dimensions, {1, 2})); + EXPECT_TRUE(CE::Math::overlaps(position, position, {1, 2}, dimensions)); +} + +TEST(math_collision, separation) { + for (const auto other : {glm::vec2{5, 0}, glm::vec2{-5, 0}, glm::vec2{0, 5}, glm::vec2{0, -5}, glm::vec2{5, 5}}) { + EXPECT_FALSE(CE::Math::overlaps({0, 0}, other, {4, 4}, {4, 4})); + EXPECT_FALSE(CE::Math::overlaps(other, {0, 0}, {4, 4}, {4, 4})); + } +} + +TEST(math_collision, touching) { + for (const auto other : {glm::vec2{4, 0}, glm::vec2{-4, 0}, glm::vec2{0, 4}, glm::vec2{0, -4}, glm::vec2{4, 4}}) { + EXPECT_FALSE(CE::Math::overlaps({0, 0}, other, {4, 4}, {4, 4})); + EXPECT_FALSE(CE::Math::overlaps(other, {0, 0}, {4, 4}, {4, 4})); + } +} + +TEST(math_collision, empty) { + for (const auto dimensions : {glm::vec2{0, 2}, glm::vec2{2, 0}, glm::vec2{-1, 2}, glm::vec2{2, -1}}) { + EXPECT_FALSE(CE::Math::overlaps({0, 0}, {0, 0}, dimensions, {4, 4})); + EXPECT_FALSE(CE::Math::overlaps({0, 0}, {0, 0}, {4, 4}, dimensions)); + } +} + +TEST(math_collision, anchors) { + constexpr CE::Rendering::Geometry::Pivot center{0.5f, 0.5f}; + constexpr CE::Rendering::Geometry::Pivot top_left{0, 0}; + constexpr CE::Rendering::Geometry::Pivot bottom_right{1, 1}; + + EXPECT_FALSE(CE::Math::overlaps({0, 0}, {3, -1}, {4, 2}, {2, 2})); + EXPECT_TRUE(CE::Math::overlaps({0, 0}, {3, -1}, {4, 2}, {2, 2}, top_left)); + EXPECT_TRUE(CE::Math::overlaps({3, -1}, {0, 0}, {2, 2}, {4, 2}, center, top_left)); + EXPECT_FALSE(CE::Math::overlaps({0, 0}, {1, 1}, {4, 2}, {2, 2}, top_left)); + + EXPECT_FALSE(CE::Math::overlaps({0, 0}, {-3, 1}, {4, 2}, {2, 2})); + EXPECT_TRUE(CE::Math::overlaps({0, 0}, {-3, 1}, {4, 2}, {2, 2}, bottom_right)); + EXPECT_TRUE(CE::Math::overlaps({-3, 1}, {0, 0}, {2, 2}, {4, 2}, center, bottom_right)); + + EXPECT_FALSE(CE::Math::overlaps({0, 0}, {3, 3}, {4, 4}, {2, 2})); + EXPECT_TRUE(CE::Math::overlaps({0, 0}, {3, 3}, {4, 4}, {2, 2}, {0.25f, 0.75f})); +} From 3542f883391d4225329dab8d5d61a5987e69c8f0 Mon Sep 17 00:00:00 2001 From: cppcooper Date: Wed, 21 Oct 2026 17:50:18 -0700 Subject: [PATCH 98/98] Fixes documentation CI test dependency setup Runs documentation tooling checks after the pinned tool environment is prepared, and executes documentation test discovery through that environment so the C++ highlighting tests can import Pygments and Tree-sitter. Validation: all 169 documentation tests and 18 stable-publication tests pass locally. Workflow YAML, test-step shell syntax, and dependency ordering also pass checks. The hosted Actions workflow has not been run for this change. --- .github/workflows/documentation.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 1f7dfdfa..c5422352 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -37,10 +37,6 @@ jobs: - name: Select documentation cache directory run: | printf 'CHERYL_DOCS_CACHE_DIR=%s/cheryl-engine/docs-tools\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" - - name: Check documentation tooling - run: | - python .github/scripts/test_sync_docs_stable.py - python -m unittest discover -s docs/site/generation/tests -p 'test_*.py' - name: Identify documentation tool cache id: tool-cache run: python -c 'import sys; sys.path.insert(0, "docs/site/generation"); from bootstrap import environment_identity; print("identity=" + environment_identity())' >> "$GITHUB_OUTPUT" @@ -55,6 +51,10 @@ jobs: key: ${{ steps.environment.outputs.environment-prefix }}-tools-${{ steps.tool-cache.outputs.identity }}-${{ hashFiles('docs/site/generation/requirements.txt', 'docs/site/generation/native-tools.json', 'docs/site/generation/bootstrap.py') }} - name: Prepare pinned documentation tools run: python docs/site/generation/bootstrap.py setup --force-pinned + - name: Check documentation tooling + run: | + python .github/scripts/test_sync_docs_stable.py + python docs/site/generation/bootstrap.py exec --force-pinned -- python -m unittest discover -s docs/site/generation/tests -p 'test_*.py' - name: Save shared documentation tools if: steps.environment.outputs.environment-cache-writer == 'true' && steps.tools.outputs.cache-hit != 'true' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0