Collapses ninja progress lines and passing gtest RUN/OK pairs in large OloEngine build and test logs before they are stored in the conversation; every warning…

OloEngine is a cross-platform (Windows / Linux) C++23 interactive-application and rendering engine. It started as a follow-along of Hazel but has since grown well past that scope into a full ECS-driven engine with a physically-based OpenGL 4.6 renderer, 2D/3D physics, dual C#/Lua scripting, networking, navigation, procedural terrain and environmental systems, a gameplay/AI stack, and an ImGui editor.
Metallic and dielectric PBR spheres (a roughness sweep) reflecting a procedural star-nest sky:

| PBR model | Animated models | 3D physics | FFT ocean |
|---|---|---|---|
![]() | ![]() | ![]() | ![]() |
| Target / directory | What it is |
|---|---|
OloEngine/ | The engine static library (OloEngine). All subsystems live under src/OloEngine/<Subsystem>/; platform glue under src/Platform/. |
OloEditor/ | ImGui-based editor (OloEditor). Panels under src/, runtime assets under assets/, a sample game under SandboxProject/. |
OloRuntime/ | Standalone game runtime (OloRuntime) that loads what the editor builds. |
OloServer/ | Headless dedicated server (OloServer) — the one target that also runs on WSL2. |
OloEngine-ScriptCore/ | C# scripting API library, built with dotnet build under any generator; C# scripting runs on Windows only. |
OloEngine-LuaScriptCore/ | Lua scripting core (all platforms). |
OloEngine/tests/ | GoogleTest suite (OloEngine-Tests). |
tools/ | Build-time tooling, notably OloHeaderTool (code generation — see Tooling). |
docs/ | Design docs, ADRs, build/testing guides, and agent rules — see Documentation. |
Supported platforms
msvc preset) or Visual Studio 2022.OloServer, but not OloEditor: WSL2's software renderer only exposes OpenGL 4.5, and the editor requires a native OpenGL 4.6 GPU.Requirements
CMakePresets.json (the msvc preset's Visual Studio 18 2026 generator only exists in 4.2+). The root CMakeLists.txt itself only requires 3.27, so a plain cmake -B build -G "Visual Studio 17 2022" still works without presets.jinja2 package (needed to build glad2).OLO_WITH_STEAM turns on when STEAMWORKS_SDK_ROOT points at a real SDK (a public/steam directory) and stays off otherwise — so without the SDK everything builds, runs and tests exactly as before, and with it installed you get the Steam path without passing a flag. The SDK itself is free — an ordinary Steam account and the Steamworks SDK Access Agreement, not the $100 Steam Direct publishing fee. Setup: docs/ops/build.md.Clone
git clone https://github.com/drsnuggles8/OloEngineBase
Most third-party dependencies are resolved from the vcpkg.json manifest and built once into vcpkg's machine-global binary cache, so a second worktree restores them in seconds instead of recompiling (issue #773). VCPKG_ROOT must point at a bootstrapped vcpkg clone — see docs/ops/build.md; a configure without it fails with a pointer there. A handful of dependencies are still fetched at configure time via FetchContent/CPM into OloEngine/vendor/ (never edit that directory — a CMake reconfigure wipes it). CMake also creates the build/ directory with the generated solution files.
CMake presets (CMakePresets.json):
| Preset | Generator / build dir | Purpose |
|---|---|---|
msvc | Visual Studio 18 2026 → build/ | Primary, default build dir |
clangcl | Ninja Multi-Config → build-clang/ | clang-cl warnings with the MSVC ABI |
clangcl-asan | Ninja Multi-Config → build-clang/ | Adds AddressSanitizer |
# Generate the Visual Studio solution
scripts\Win-GenerateProjectVS2026.bat # or Win-GenerateProjectVS2022.bat
# Build a target
cmake --build build --target OloEditor --config Debug --parallel
cmake --build build --target OloEngine-Tests --config Debug --parallel
cmake --build build --target OloRuntime --config Debug --parallel
cmake --build build --target OloServer --config Debug --parallel
# ClangCL (configure once, then build)
cmake --preset clangcl
cmake --build build-clang --target OloEngine-Tests --config Debug --parallel
Working directory matters.
OloEditor,OloRuntime, andOloServerresolve assets, shaders, and Mono assemblies relative toOloEditor/. Always run them withcwd = OloEditor/. The VS Code tasks already do this.
VS Code users — predefined tasks in .vscode/tasks.json wrap all of the above: build-oloeditor-debug, run-oloeditor-debug, build-tests-debug, run-tests-debug, configure-clangcl, etc. Run them from the Command Palette (Ctrl+Shift+P → Tasks: Run Task).
CLion users — open the repo folder, let CLion configure the CMake project, then set the working directory of the OloEditor run configuration to the OloEditor/ folder.
The full per-OS build matrix (including Linux and WSL specifics) lives in docs/ops/build.md.
Entity wrapper. The hottest update loops use EnTT owning groups for packed iteration.AssetManager.LocalizedTextComponent.OloServer provides the headless dedicated-server target.The suite (OloEngine-Tests, GoogleTest) spans two independent axes:
"Functional" tag) driving real Scene::OnUpdateRuntime ticks across Animation × Physics × Scripting × Networking × Audio × Asset × Nav × Save-game × Gameplay × AI seams.# Run everything, or a single test
build\OloEngine\tests\Debug\OloEngine-Tests.exe
build\OloEngine\tests\Debug\OloEngine-Tests.exe --gtest_filter=SuiteName.TestName
The rationale, per-layer reference, classification rules, and anti-patterns live in docs/testing.md and docs/agent-rules/testing-architecture.md.
These gate every change and run on GitHub-hosted runners.
| Workflow | Status | Covers |
|---|---|---|
| Windows | MSVC build + full suite via CTest | |
| Sanitizers | ASan (Windows/clang-cl), ASan+LSan / UBSan / TSan (Linux/Clang) | |
| SonarCloud Scan | Static analysis (see sonarqube-review-alignment.md) | |
| Pre-commit checks | Formatting, YAML, test-catalogue classification |
Cron times are UTC — GitHub's scheduler does not observe DST, so local times shift by an hour across the year. Every one of these is also workflow_dispatch-able from the Actions tab. Scheduled runs may start tens of minutes late under load.
| Workflow | Status | Cadence (UTC) | Runs on | Covers |
|---|---|---|---|---|
| GPU Conformance (AMD) | nightly 00:47 | self-hosted Linux / AMD Navi 10 | The renderer's visual, golden and perf layers against a real GPU — they skip everywhere else. See self-hosted-gpu-runner.md | |
| Cross-Vendor Conformance | nightly 03:47 | GitHub-hosted Windows | L1/3/5/6/8 against Mesa llvmpipe (software) | |
| Fuzz Smoke | nightly 04:13 | GitHub-hosted Windows | Parser / deserializer fuzzing | |
| Flaky repro (#281) | nightly 07:00 | GitHub-hosted Windows | Repeat-runs a known-flaky test to hunt issue #281 | |
| Video (FFmpeg backend) | weekly, Mon 06:00 | GitHub-hosted matrix | OLO_VIDEO_FFMPEG=ON build + decode tests | |
| reflection-status-check | monthly, 1st 09:07 | GitHub-hosted Linux | Reminder to re-check C++26 reflection toolchain support |
Hardware-GPU vendor coverage is AMD-only. The goldens in assets/tests/golden/ are baselined on NVIDIA, but only ever from a developer machine — there is no NVIDIA or Intel CI. llvmpipe is a third software implementation, not a second vendor, so it catches spec-compliance drift rather than driver divergence.
OloHeaderTool (tools/OloHeaderTool/) scans OloEngine/src/ and generates, at build time (via the GenerateBindings target):
OLO_PROPERTY annotations.AllComponents type list, the save-game capture/restore lists, the OnComponentAdded/OnComponentRemoved no-op handlers, and per-component scene serialize/deserialize blocks — all derived from the struct *Component definitions.This collapses what used to be several hand-maintained, easy-to-forget touch-points into codegen: an all-trivial component now round-trips through scenes, save-games, and scripting with little or no hand-written glue. See CLAUDE.md for the exact contract when adding or changing a component.
Most dependencies come from the vcpkg.json manifest, pinned as a whole by its builtin-baseline registry commit and built into vcpkg's binary cache. The ones marked (in-tree) below are still fetched via FetchContent/CPM into OloEngine/vendor/, each for a specific reason documented in that file's header comment — briefly: sol2 and Lua are pinned to a tested pair, stb's port is ~2 years behind on an image decoder, ImGuizmo's port would drag in a second Dear ImGui, and the single-header / forked / generator-driven ones have no port at all. OpenUSD and FFmpeg have their own build paths (see CLAUDE.md).
Core — entt (ECS) · glm (math) · spdlog (logging) · yaml-cpp (serialization) · nlohmann/json (tools / IPC) · choc (utilities, in-tree) · atomic_queue (lock-free MPMC queue) · meshoptimizer
Rendering — glad (in-tree) · glfw · assimp · stb (in-tree) · zlib
Physics — Jolt Physics (3D) · box2d (2D)
Networking — GameNetworkingSockets · libsodium · protobuf
Navigation — recastnavigation
Audio — miniaudio
Scripting — sol2 (in-tree) · lua (in-tree) · Mono (C# runtime, manually integrated)
UI & editor — imgui (in-tree) · imguizmo (in-tree)
Development & profiling — tracy · googletest · filewatch (in-tree)
We enforce a small set of formatting rules via pre-commit:
python -m pip install --user pre-commit
pre-commit install # enable the git hook (run in the repo root)
pre-commit run --all-files # optional: apply across the whole repo
Included hooks: trailing-whitespace, end-of-file-fixer, and clang-format for C/C++ (uses the repo-root .clang-format). An .editorconfig mirrors the whitespace rules for editors. The hooks ignore vendor/, build/, and IDE metadata folders. A GitHub Action runs pre-commit on every push and PR.
Everything under docs/ — start at docs/README.md for the full index. Highlights:
CLAUDE.md — contributor/agent playbook: build, test, and the component cross-binding contract.hooks/register.ts 52 lines1import type { Register } from 'claude-code'
2
3import { compressLog } from './compress'
4
5// Tool results that carry build and test logs.
6const LOG_TOOLS = new Set(['Bash', 'PowerShell', 'Read'])
7
8type TextBlock = { type: 'text'; text: string }
9type ToolResultBlock = { type: 'tool_result'; tool_use_id?: string; content?: string | TextBlock[]; is_error?: boolean }
10
11export const register: Register = on => {
12 // The row a tool result is stored as: what the model reads from now on. Hooks on
13 // tool.call (olo-output-truth among them) have already seen the full text by then.
14 on('session.append', ($, e, next) => {
15 const origin = e.origin as { kind?: string; tool?: string }
16 if (e.door !== 'tool-result' || origin.kind !== 'tool' || !LOG_TOOLS.has(String(origin.tool))) {
17 return next(e)
18 }
19 let changed = false
20 const content = e.message.content.map(block => {
21 if (block.type !== 'tool_result') {
22 return block
23 }
24 const result = block as ToolResultBlock
25 if (typeof result.content === 'string') {
26 const squeezed = compressLog(result.content)
27 if (squeezed === undefined) {
28 return block
29 }
30 changed = true
31 return { ...result, content: squeezed.text }
32 }
33 if (Array.isArray(result.content)) {
34 const parts = result.content.map(part => {
35 if (part.type !== 'text') {
36 return part
37 }
38 const squeezed = compressLog(part.text)
39 if (squeezed === undefined) {
40 return part
41 }
42 changed = true
43 return { ...part, text: squeezed.text }
44 })
45 return { ...result, content: parts }
46 }
47 return block
48 })
49 return changed ? next({ ...e, message: { ...e.message, content: content as typeof e.message.content } }) : next(e)
50 })
51}
52hooks/compress.ts 80 lines1// Collapses the lines of a big build or test log that carry no information once you know
2// how many there were: ninja's progress lines and gtest's passing RUN/OK pairs. Every other
3// line (warnings, errors, failures, a test's own output, summaries) is kept verbatim and
4// in order, so nothing a reader acts on is lost.
5
6export const MIN_CHARS = 8000
7const MIN_SAVING = 0.25
8
9const GUTTER = /^ *\d+\t/
10const NINJA_PROGRESS = /^\[\d+\/\d+\] (?:Building|Linking|Generating|Running|Copying|Automatic|Re-running|Scanning|Creating|Install|Performing|No install)\b/
11const GTEST_RUN = /^\[ RUN \] (\S+)/
12const GTEST_OK = /^\[ OK \] (\S+)/
13const GTEST_SEPARATOR = /^\[----------\] /
14
15export type Compressed = { text: string; progress: number; passing: number; separators: number }
16
17export function compressLog(text: string): Compressed | undefined {
18 if (text.length < MIN_CHARS) {
19 return undefined
20 }
21 const lines = text.split('\n')
22 // Matched without CR, Read's line gutter and ANSI colour (gtest colours its markers); the
23 // lines kept are written back as they came.
24 const bare = lines.map(l => l.replace(/\r$/, '').replace(GUTTER, '').replace(/\u001b\[[0-9;]*[A-Za-z]/g, ''))
25 const out: string[] = []
26 let progress = 0
27 let passing = 0
28 let separators = 0
29
30 for (let i = 0; i < lines.length; i += 1) {
31 const line = bare[i] ?? ''
32
33 // A run of ninja progress lines: keep the first and the last, count the middle.
34 if (NINJA_PROGRESS.test(line)) {
35 let j = i
36 while (j + 1 < lines.length && NINJA_PROGRESS.test(bare[j + 1] ?? '')) {
37 j += 1
38 }
39 const runLength = j - i + 1
40 if (runLength > 2) {
41 out.push(lines[i] ?? '', `… ${runLength - 2} ninja progress lines …`, lines[j] ?? '')
42 progress += runLength - 2
43 } else {
44 out.push(...lines.slice(i, j + 1))
45 }
46 i = j
47 continue
48 }
49
50 // A test that passed with no output of its own: RUN immediately followed by its OK.
51 const run = GTEST_RUN.exec(line)
52 if (run !== null && GTEST_OK.exec(bare[i + 1] ?? '')?.[1] === run[1]) {
53 passing += 1
54 i += 1
55 continue
56 }
57
58 if (GTEST_SEPARATOR.test(line)) {
59 separators += 1
60 continue
61 }
62
63 out.push(lines[i] ?? '')
64 }
65
66 // Say where the collapsed passing tests were, once, not per line.
67 const removedLines = progress + passing * 2 + separators
68 if (removedLines === 0) {
69 return undefined
70 }
71 const parts = [
72 progress > 0 ? `${progress} ninja progress lines` : '',
73 passing > 0 ? `${passing} passing tests (their RUN/OK lines)` : '',
74 separators > 0 ? `${separators} gtest suite separators` : '',
75 ].filter(Boolean)
76 out.push(`[olo-context-saver: collapsed ${parts.join(', ')}; every warning, error, failure and summary line above is verbatim]`)
77 const result = out.join('\n')
78 return result.length <= text.length * (1 - MIN_SAVING) ? { text: result, progress, passing, separators } : undefined
79}
80