Purpose: CORE Rules + Examples pillars. Contains every C++ constraint, coding pattern, naming convention, and known defect that agents must honor when analyzing crashes, proposing fixes, or generating code in the
engine_demoworkspace.Source:
specs/constitution.md,.github/instructions/cpp-snippets.instructions.md, observed patterns ininclude/engine_demo/**/*.handsrc/engine_demo/**/*.cpp.
These 8 articles are non-negotiable. Any agent output that violates them is automatically rejected by the reviewer chat mode.
- Compile flags:
-fno-exceptions(GCC/Clang),/EHs-c-(MSVC) - Forbidden tokens:
try,catch,throw,noexcept(false),std::exception - Error handling: return
nullptr, status enum, oreastl::expected<T, status>
- Compile flags:
-fno-rtti(GCC/Clang),/GR-(MSVC) - Forbidden tokens:
dynamic_cast,typeid - Type discrimination: tagged unions,
eastl::variant, typed-handle patterns
- Default containers:
eastl::vector,eastl::string,eastl::unique_ptr,eastl::shared_ptr,eastl::function,eastl::span,eastl::optional,eastl::variant std::containers permitted ONLY in interop layers marked// interop boundary- Non-allocating
std::is always allowed:<cstdint>,<cstddef>,<utility>,<type_traits>,<cmath>,<cstring>
- Every EASTL container takes an explicit allocator at construction
- Source of truth:
engine_demo::allocatorininclude/engine_demo/allocator.h - Adapter:
engine_demo::eastl_allocator_refbridges to EASTL's allocator concept - Default-constructed containers are forbidden in committed code
- Simulation accumulators:
double(neverfloat— this IS BUG-002) - RNG: explicitly seeded
std::mt19937(viaengine_demo::sim::rng); neverstd::random_devicein sim paths - Container iteration: deterministic order; never
eastl::unordered_mapkeyed on pointer identity in sim paths (this IS BUG-004)
- Frame budget: ≤16.67 ms at 60 FPS, ≤8.33 ms at 120 FPS
- No allocation in inner loops; pool or arena up front
- No locks in render thread; use lockless ring buffers for cross-thread handoff
- Branch-prediction-friendly idioms preferred
- Every public function has at least one GTest covering happy + edge cases
- Implementation follows test (test file written first)
- CI gates on green tests + clang-tidy clean
DISABLED_prefix = seeded bug regression (expected to fail until fix lands)
- Spec-Kit pauses for human approval between
/plan→/tasksand between each/implement - Coding Agent handoffs require human review before merge
- Reviewer chat mode runs over every session bundle before delivery
| Element | Convention | Examples |
|---|---|---|
| Namespaces | snake_case, nested |
engine_demo, engine_demo::ecs, engine_demo::physics, engine_demo::sim |
| Classes / structs | snake_case |
constraint_solver, game_loop, frame_budget, entity_handle |
| Member variables | m_ prefix |
m_offset, m_bodies, m_accumulator_seconds, m_alloc |
| Constants | k + PascalCase |
kMaxEntities, kTrailLength, kFixedStepSeconds |
| Enum classes | snake_case type + values |
alloc_status::out_of_memory, scene_kind::rope |
| Free functions | snake_case |
align_up, derive_subseed |
| Macros | UPPER_SNAKE (avoid in new code) | — |
| Files | snake_case.h / .cpp |
game_loop.h, constraint.cpp |
Apply to:
- Factory functions (
make_*,create_*) - Functions returning a status / result / error type
- Functions whose only purpose is returning a value
- Classes that should never be silently discarded (e.g.,
class [[nodiscard]] allocator)
Apply to:
- Move constructors and move-assignment operators
- Swap functions
- Destructors (implicit, but never annotate otherwise)
- All methods in this codebase (since exceptions are disabled globally)
Required comment on any line or block that uses std:: types that normally violate Article 3.
Current interop boundaries in the codebase:
sim::rng— usesstd::mt19937(EASTL has no Mersenne Twister)- Sandbox
app.cpp— uses raylib C API (labeled interop boundary)
Pattern 1: Return nullptr
void* allocate(size_t n, size_t alignment) noexcept {
if (bad_input) return nullptr;
...
}
Pattern 2: Return status enum
enum class alloc_status : uint8_t { ok, out_of_memory, invalid_alignment, invalid_size };
Pattern 3: Guard-clause early returns
void destroy_entity(entity_handle h) noexcept {
if (h.index >= kMaxEntities) return;
auto& s = m_slots[h.index];
if (!s.alive || s.generation != h.generation) return;
// ... proceed with valid state
}
Pattern 4: Sentinel / default values
entity_handle{} — index=0, generation=0, is_valid()=false
const body* try_get_body(id) — returns nullptr if not found
Six intentionally planted defects. Do NOT fix — they are the demo material.
- File:
src/engine_demo/allocator.cpp - CWE: CWE-787 (Out-of-bounds Write)
- Mechanism: Bounds check uses pre-alignment offset (
m_offset + n > m_capacity) but alignment padding computed AFTER the check can push the actual write past the arena tail - Trigger: Third aligned allocation against a tight arena
- Symptom: Crash (access violation / segfault)
- Fixture:
fixtures/crash_dumps/BUG-001/(.dmp+.pdb) - Fix: Compute aligned offset FIRST, then check
(aligned_offset + n) <= m_capacity
- File:
src/engine_demo/sim/game_loop.cpp - CWE: CWE-681 (Incorrect Conversion)
- Mechanism: Accumulator is
floatinstead ofdouble; implicit narrowing on every+=accumulates representation error at ~ULP(float)/step - Trigger: ~30 seconds of simulated time at 1/60s steps
- Symptom: Substep count oscillates; replay diverges from reference
- Fix: Change
m_accumulator_secondsfromfloattodouble
- File:
src/engine_demo/ecs/world.cpp - CWE: CWE-416 (Use After Free)
- Mechanism:
destroy_entity()setsalive=falsebut does NOT bumpgeneration. Nextcreate_entity()reuses the slot with same generation — stale handles passis_alive() - Trigger: Destroy entity, create new entity that recycles same slot, use old handle
- Symptom: Stale handle accesses wrong logical entity's data
- Fix: Add
++s.generation;indestroy_entity()before settingalive=false
- File:
src/engine_demo/physics/constraint.cpp - CWE: N/A (determinism violation)
- Mechanism:
solve()reads body state viam_bodies.find()on aneastl::hash_map. Hash collisions differ across runs → projection order changes → solver converges to slightly different positions - Trigger: Multiple bodies with hash-colliding IDs; different run = different order
- Symptom: Replay diverges by O(1e-9) per frame, accumulating over seconds
- Fix: Sort constraints by
(min(a,b), max(a,b))at insertion; read body state via deterministic index instead of hash-map lookup
- File:
src/engine_demo/sim/rng.cpp - CWE: CWE-197 (Numeric Truncation Error)
- Mechanism:
uint64_t seedforwarded asstatic_cast<uint32_t>(seed)tomt19937constructor — high 32 bits silently dropped - Trigger: Two seeds sharing low 32 bits produce identical RNG streams
- Symptom: Collision in per-session entropy; security + determinism regression
- Fix: Use
derive_subseed(seed)which XOR-folds high half into low half
- File:
src/engine_demo/frame_budget.cpp - CWE: CWE-193 (Off-by-one Error)
- Mechanism:
record_sample()writes atm_indexthen increments;rolling_average()iterates[0, m_count). On first frame, slot 0 is counted twice (written + initial zero not cleared) - Trigger: First frame after construction
- Symptom: Non-zero average from an idle engine
- Fix: Pre-increment index before write, OR track
m_countasmin(writes, window_size)and iterate from(m_index - m_count)wrapping
- File:
src/engine_demo/sim/rng.cpp - Why it looks suspicious: Function takes
uint64_t, returnsuint32_t— looks like the same pattern as BUG-005 - Why it's safe: XOR-folds
high ^ low— preserves entropy from both halves - Learner action: Dismiss with a one-line comment near the suspect line
- File:
src/engine_demo/physics/constraint.cpp - Why it looks suspicious: Trailing iteration appears to read past
m_constraints.end() - Why it's safe: Sentinel iteration exits before any buffer read
- Learner action: Dismiss with a one-line comment near the suspect line
| Location | std:: type used |
Reason |
|---|---|---|
sim::rng |
std::mt19937 |
EASTL does not ship a Mersenne Twister engine |
sim::rng |
std::mt19937::result_type |
Required for seed type |
frame_budget |
double[256] (C array) |
Zero-allocation ring; not an EASTL container |
Sandbox app.cpp |
raylib C API | Rendering interop boundary (not engine_demo library) |
| Headers | <cstdint>, <cstddef>, <cstring>, <cmath> |
Non-allocating standard library (always allowed) |
| Flag | Platform | Effect |
|---|---|---|
-fno-exceptions |
GCC / Clang | Disables exception support |
/EHs-c- |
MSVC | Disables exception support |
-fno-rtti |
GCC / Clang | Disables RTTI |
/GR- |
MSVC | Disables RTTI |
-Wall -Wextra -Werror |
GCC / Clang | All warnings as errors |
/W4 /WX |
MSVC | All warnings as errors |
-Wsign-conversion |
GCC / Clang | Signed/unsigned mismatch |
-Wunused-private-field |
Clang | Unused member detection |
- C++20 (
CMAKE_CXX_STANDARD 20,CMAKE_CXX_STANDARD_REQUIRED ON) - No extensions (
CMAKE_CXX_EXTENSIONS OFF)
- MSVC 19.38+ (Visual Studio 2022 17.8+)
- GCC 13+
- Clang 17+ (Apple Clang 17+ on macOS)
Agents (crash-engineer, crash-qa, crash-validator) must reject or rewrite any code containing these patterns:
| Anti-pattern | Required replacement |
|---|---|
std::vector<T> |
eastl::vector<T, eastl_allocator_ref> |
std::string |
eastl::string (with allocator) |
std::unique_ptr<T> |
eastl::unique_ptr<T> |
std::map<K,V> / std::unordered_map<K,V> |
eastl::hash_map<K,V,...,eastl_allocator_ref> |
try / catch / throw |
Status enum return or nullptr |
dynamic_cast<T*>(x) |
Typed-handle pattern or eastl::variant |
(T)x (C-style cast) |
static_cast<T>(x) |
new T / delete p |
eastl::make_unique<T>(...) or allocator |
float for sim accumulator |
double |
std::random_device in sim |
engine_demo::sim::rng with explicit seed |
using namespace std; |
Never (especially in headers) |
#include <bits/stdc++.h> |
Include only what you use |
| Default-constructed EASTL container | Pass explicit eastl_allocator_ref |
typedef |
using alias |
std::bind |
Lambda |
| Idiom | When |
|---|---|
| Range-for | Iterating any container |
| Structured bindings | Tuple/pair returns, hash_map iteration |
if constexpr |
Compile-time branches (over SFINAE) |
eastl::span<T> |
Non-owning array views in function signatures |
| C++20 Concepts | Template constraints (document intent) |
constexpr |
Wherever feasible for compile-time evaluation |
constinit |
Global state with non-trivial init |
| Guard clauses | Early-return for invalid input (not deep nesting) |
When analyzing a crash dump from this codebase, agents should know:
-
The allocator is a bump-pointer arena — deallocate is a no-op. Memory is only reclaimed when the allocator object is destroyed. An OOB write in the arena corrupts adjacent allocations, not freed memory.
-
Entity handles are generational — a stale handle crash means the generation check failed (BUG-003) or was bypassed.
-
The physics solver mutates bodies in-place — a crash in
solve()likely means a body was removed mid-iteration or the hash_map was invalidated. -
RNG state is per-instance — if two systems share an
rnginstance, their streams are entangled (not a bug, but a common confusion point). -
Frame budget never allocates at runtime — if you see an allocation crash in
frame_budget, it happened during construction, not duringrecord_sample(). -
The sandbox is an interop boundary — crashes in
app.cppmay involve raylib's internal state, not engine_demo logic. Distinguish raylib frames from engine_demo frames in the call stack.