engine
Daemon Engine — C++20 game engine with DirectX 11, V8 scripting
What it does
- The engine is a C++20 native game/runtime used as the core of the AGENTS platform. It presents an async, worker/main-thread architecture optimized for low-latency rendering, audio and entity updates while allowing high-level logic to run in V8-based JavaScript workers.
- Key runtime features: double-buffered state containers for lock-free reads and writes, DirectX 11 renderer (Renderer module), V8-based Script module for game logic, audio playback with spatial positioning, input handling, resource management, and a small network/Platform layer for connectivity.
Architecture philosophy
- Separation of concerns between “main-thread authoritative” systems (Renderer, Audio, Resource) and “worker-side” command producers (Script, game logic).
- Double-buffered POD state objects (EntityState, AudioState) are written by worker threads into back buffers and consumed lock-free by main-thread subsystems after an explicit SwapBuffers() call.
- Minimal locking: brief locked swaps only at frame boundaries; per-key dirty-tracking to reduce copy cost for sparse updates.
- Simple, memory-efficient data structures (POD where possible) to keep copy and cache behavior predictable.
Tech stack
- Language: C++20
- Graphics: DirectX 11 (Renderer module)
- Scripting: V8 (Script module)
- Build: MSBuild / Visual Studio (x64)
- Platform: Windows x64 (Windows SDK / DirectX runtime)
- Core modules: Audio, Core, Entity, Input, Math, Network, Platform, Renderer, Resource, Script, UI, Widget
Module Architecture
Section titled “Module Architecture”Key modules/subsystems and responsibilities
- Core
- EngineCommon, StateBuffer template, BufferParser/BufferWriter: binary parsing/writing helpers and shared types.
- Coordinates double-buffering primitives used across modules.
- Script
- Hosts V8 instances and exposes engine bindings to JavaScript. Runs logic in worker threads that write to back buffers.
- Entity
- Holds EntityID and EntityState definitions. Worker-side updates entity state into EntityStateBuffer; Renderer reads front buffer for rendering.
- Audio
- AudioState and AudioStateBuffer: spatial audio states; main thread audio processing consumes front buffer.
- Renderer
- Pulls EntityState front buffer and resources for draw calls; responsible for DirectX 11 device, swap chain, and per-frame rendering.
- Resource
- Manages loading textures, meshes, audio assets, and returns handles (textureId) used by EntityState / Audio.
- Input
- Device handling, controllers (AnalogJoystick), and normalized inputs for gameplay code.
- Network / Platform
- Platform-specific sockets and system services. Connects to external orchestrator/broker (KĀDI) for orchestration and telemetry.
- UI / Widget
- Immediate-mode or retained UI elements; consumes Entity and Resource outputs to present debug controls or in-game HUD.
Data flow (typical)
- JavaScript (Script module, worker thread) writes to back buffers:
- EntityStateBuffer::GetBackBuffer()->operator[] =
- AudioStateBuffer::GetBackBuffer()->operator[] =
- Call buffer->MarkDirty(id) when doing O(d) optimized updates.
- EntityStateBuffer::GetBackBuffer()->operator[] =
- At frame boundary main thread calls SwapBuffers() on each StateBuffer:
- Copies/commits dirty entries from back -> front under brief lock.
- Renderer / Audio / UI read lock-free from front buffers:
- Iterate front buffer entries and perform processing / draw / playback.
- Resource module provides texture/audio handles to states via callbacks/handles.
Double-buffer patterns are pervasive: see EntityState / AudioState pairings and the specialized AudioStateBuffer typedef.
Module Architecture (data-flow diagram)
Section titled “Module Architecture (data-flow diagram)”(visualization of dataflow — worker scripts -> back buffers -> SwapBuffers -> main-thread subsystems)
- Script (V8, worker) —> BackBuffer (EntityState / AudioState) —> SwapBuffers() —> FrontBuffer —> Renderer / Audio / UI
Key Classes
Section titled “Key Classes”Below are the most important classes/types you will encounter when modifying or extending engine behavior. Snippets are taken from the source headers to preserve exact member names and semantics.
- AudioState (Engine/Audio/AudioState.hpp)
struct AudioState{ SoundID soundId; // Unique sound identifier (uint64_t from AudioTypes.hpp) std::string soundPath; // File path to audio resource (relative to Run/ directory) Vec3 position; // 3D world-space position (X-forward, Y-left, Z-up) float volume; // Playback volume (0.0 - 1.0, clamped by AudioSystem) bool isPlaying; // Playback state (true = playing, false = stopped) bool isLooped; // Loop flag (true = loop continuously, false = one-shot) bool isLoaded; // Load state (true = ready, false = loading) bool isActive; // Active flag (true = valid, false = marked for removal)
// Default constructor (safe initial state) AudioState() : soundId(0) , soundPath("") , position(Vec3::ZERO) , volume(1.0f) , isPlaying(false) , isLooped(false) , isLoaded(false) , isActive(true) // New entries are active by default { }
// Explicit constructor (for command processing)- AudioStateBuffer (Engine/Audio/AudioStateBuffer.hpp) — typedef of the generic StateBuffer:
using AudioStateBuffer = StateBuffer<AudioStateMap>;AudioStateBuffer exposes GetFrontBuffer(), GetBackBuffer(), SwapBuffers(), EnableDirtyTracking(bool), MarkDirty(SoundID), etc., inherited from StateBuffer
- EntityState and EntityID (Engine/Entity/EntityState.hpp, Engine/Entity/EntityID.hpp)
using EntityID = uint64_t;struct EntityState{ Vec3 position; EulerAngles orientation; Rgba8 color; float radius; std::string meshType; bool isActive; std::string cameraType; uint64_t textureId;
EntityState() : position(Vec3::ZERO), orientation(EulerAngles::ZERO), color(Rgba8::WHITE), radius(1.0f), meshType("cube"), isActive(true), cameraType("world"), textureId(0) { }- BufferParser / BufferWriter (Engine/Core)
- Low-level binary readers/writers used by Resource and network serialization:
class BufferParser{public: BufferParser(uint8_t const* data, size_t size); BufferParser(std::vector<uint8_t> const& buffer);
// Primitives (10 types) uint8_t ParseByte(); char ParseChar(); unsigned short ParseUshort(); short ParseShort(); unsigned int ParseUint32(); int ParseInt32(); uint64_t ParseUint64(); int64_t ParseInt64(); float ParseFloat(); double ParseDouble();
// Strings void ParseZeroTerminatedString(std::string& out_str); void ParseLengthPrecededString(std::string& out_str);
// Engine semi-primitives Vec2 ParseVec2(); Vec3 ParseVec3(); IntVec2 ParseIntVec2(); Rgba8 ParseRgba8(); AABB2 ParseAABB2(); Plane2 ParsePlane2(); Vertex_PCU ParseVertexPCU();
// Position control size_t GetCurrentPosition() const; void SetCurrentPosition(size_t position);
// Endianness control void SetEndianMode(eEndianMode mode); eEndianMode GetEndianMode() const; ...class BufferWriter{public: explicit BufferWriter(std::vector<uint8_t>& buffer);
// Primitives (10 types) void AppendByte(uint8_t value); void AppendChar(char value); void AppendUshort(unsigned short value); void AppendShort(short value); void AppendUint32(unsigned int value); void AppendInt32(int value); void AppendUint64(uint64_t value); void AppendInt64(int64_t value); void AppendFloat(float value); void AppendDouble(double value);
// Strings void AppendZeroTerminatedString(std::string const& str); void AppendLengthPrecededString(std::string const& str);
// Engine semi-primitives void AppendVec2(Vec2 const& v); void AppendVec3(Vec3 const& v); void AppendIntVec2(IntVec2 const& v); void AppendRgba8(Rgba8 const& color); void AppendAABB2(AABB2 const& box); void AppendPlane2(Plane2 const& plane); void AppendVertexPCU(Vertex_PCU const& vert); ...- AnalogJoystick (Engine/Input/AnalogJoystick.hpp)
- Controller analog stick helper with dead zone correction:
class AnalogJoystick{public: // Get the dead zone-corrected joystick position // // @return Vec2 Corrected 2D position in normalized space [-1, 1] for both axes Vec2 GetPosition() const; ...V8 Scripting API
Section titled “V8 Scripting API”How scripts interact with the engine (worker/main-thread model)
- The Script module hosts V8 and binds a minimal runtime API to JavaScript that follows the double-buffer pattern:
- Worker scripts can obtain references to back buffers (plain JS objects or typed wrappers), mutate them and inform the engine about which keys changed.
- Main thread is authoritative: scripts must not attempt to read front buffers directly (race conditions); instead, readbacks go through explicit frame-synced callbacks or messages.
Common scripting patterns (examples)
- Writing an Audio command into the back buffer from a worker script:
// Example worker script pseudocode — demonstrates the intended pattern// The concrete binding names may vary; you will find the exact names in Script bindings.let back = Audio.getBackBuffer(); // returns a JS Map-like view keyed by SoundIDback.set(42, { soundId: 42, soundPath: "Data/Audio/explosion.mp3", position: { x: 10.0, y: 0.0, z: 2.0 }, volume: 0.8, isPlaying: true, isLooped: false, isLoaded: true, isActive: true});Audio.markDirty(42); // Hint to engine to copy only this entry during SwapBuffers()- Updating an Entity state:
let eBack = Entity.getBackBuffer();eBack.set(1001, { position: { x: 1.0, y: 2.0, z: 3.0 }, orientation: { yaw: 45, pitch: 0, roll: 0 }, color: { r:255,g:128,b:64,a:255 }, meshType: "cube", radius: 1.5, isActive: true, textureId: 123 // texture handle returned earlier from Resource.loadTexture(...)});Entity.markDirty(1001);Key scripting rules
- Only mutate back buffers from script/worker threads.
- Call MarkDirty(id) when O(d) mode is enabled to limit copies to changed keys.
- Do not rely on immediate main-thread effects; changes become visible after the next SwapBuffers() which is scheduled per-frame on the main thread.
- For one-off RPC calls (load a texture, create audio handle), Script bindings provide asynchronous APIs that call into Resource/Audio and return handles to the worker when complete.
Where to find binding points
- Check the Script module bindings for the exact function and object names exposed to V8. Look for the code that marshals StateBuffer
into V8 wrappers and the functions that call StateBuffer::MarkDirty.
Build & Configuration
Section titled “Build & Configuration”Build system
- Primary build: MSBuild / Visual Studio solution (x64). Use Visual Studio 2022 or newer with C++20 toolset.
- Compiler standard: C++20 (language features enabled in project settings).
- Project layout: engine code under Code/Engine/… with modules organized as static/dynamic libraries within the solution.
Dependencies
Section titled “Dependencies”- Windows SDK (Win32, WinRT headers as needed)
- DirectX 11 runtime and headers (D3D11.lib, D3DCompiler, DXGI)
- V8 runtime libraries and include headers (link v8*.lib)
- Standard C++ STL (iostream, vector, unordered_map, string, etc.)
Platform requirements
- Target: Windows x64 (Windows 10 / 11)
- Development: Visual Studio 2022+ with Windows SDK
- Runtime: DirectX 11 capable GPU/drivers; V8 native runtime available / deployed with the executable.
Recommended configuration notes
- Use x64 Release configuration with /O2 for production builds.
- Keep V8 built with same CRT linkage as the engine to avoid ABI/CRT mismatches.
- Ensure lib dependencies (DirectX, V8) are present on the linker’s Additional Dependencies and that include paths point to their headers.
- Check the Script module configuration for V8 initialization flags and snapshot generation options (if snapshotting is used).
Integration with AGENTS (KĀDI broker/platform)
Section titled “Integration with AGENTS (KĀDI broker/platform)”How the engine connects to the KĀDI broker
- The Network and Platform modules provide the transport layer used to integrate the engine with the AGENTS orchestration environment (KĀDI broker).
- Integration points (typical):
- Network sockets or platform-specific IPC are used to register the engine instance with the broker and receive orchestration messages.
- The Script module can expose bridge functions so the broker can invoke scripts or read/write back buffers remotely (for telemetry, debugging, or distributed control).
- StateBuffer front buffers represent the authoritative runtime snapshot that can be published to KĀDI consumers for monitoring or inspection without blocking the main loop.
Recommended integration approach
- Publish summarized front-buffer snapshots (EntityState / AudioState aggregated views) to the broker at a controlled interval to avoid saturating network/IPC.
- Use Resource module hooks to handle asset requests from KĀDI: broker requests may result in the engine calling Resource::Load and returning handles via the network.
- Prefer broker-driven control via Script-bound RPCs: the broker requests that a script mutate back buffers, then the engine’s normal SwapBuffers() path makes the change live.
Operational notes
- Keep broker interactions non-blocking: use async IO in Network/Platform and callbacks into Script to schedule work on worker threads.
- Use MarkDirty + O(d) enabled StateBuffers to minimize data transferred to KĀDI when only a small portion of state changes.
Where to start when modifying
Section titled “Where to start when modifying”- To add a new double-buffered state type:
- Define a POD struct (like AudioState / EntityState) in the appropriate module under Code/Engine/
. - Add a StateBuffer specialization typedef (using StateBuffer
). - Expose GetBackBuffer(), MarkDirty() bindings in Script to allow worker mutations.
- Hook front buffer consumption into main-thread subsystem (Renderer, Audio or other).
- Define a POD struct (like AudioState / EntityState) in the appropriate module under Code/Engine/
- To change Swap behavior:
- Inspect the generic StateBuffer
implementation (Code/Engine/Core/StateBuffer.hpp/cpp). - Use EnableDirtyTracking(true) when updates are sparse; test correctness with aggressive concurrency scenarios.
- Inspect the generic StateBuffer
If you need precise binding names or locations for the V8 wrappers, search the Script module source for the V8 binding code (functions that call v8::FunctionTemplate, v8::ObjectTemplate and the code that wraps StateBuffer