Files
SpacetimeDB/modules/benchmarks-cpp/src/ia_loop.cpp
T
Jason Larabie 52b6c66fa1 Add C++ Bindings (#3544)
# Description of Changes

This adds C++ server bindings (/crate/bindings-cpp) to allow writing C++
20 modules.

- Emscripten WASM build system integration with CMake
- Macro-based code generation (SPACETIMEDB_TABLE, SPACETIMEDB_REDUCER,
etc)
- All SpacetimeDB types supported (primitives, Timestamp, Identity,
Uuid, etc)
- Product types via SPACETIMEDB_STRUCT
- Sum types via SPACETIMEDB_ENUM
- Constraints marked with FIELD* macros

# API and ABI breaking changes

None

# Expected complexity level and risk

2 - Doesn't heavily impact any other areas but is complex macro C++
structure to support a similar developer experience, did have a small
impact on init command

# Testing

- [x] modules/module-test-cpp - heavily tested every reducer
- [x] modules/benchmarks-cpp - tested through the standalone (~6x faster
than C#, ~6x slower than Rust)
- [x] modules/sdk-test-cpp
- [x] modules/sdk-test-procedure-cpp
- [x] modules/sdk-test-view-cpp  
- [x] Wrote several test modules myself
- [x] Quickstart smoketest [Currently in progress]
- [ ] Write Blackholio C++ server module

---------

Signed-off-by: Jason Larabie <jason@clockworklabs.io>
Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com>
Co-authored-by: Ryan <r.ekhoff@clockworklabs.io>
Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com>
2026-02-07 04:26:45 +00:00

444 lines
16 KiB
C++

//! STDB module used for benchmarks based on "realistic" workloads we are focusing in improving.
//! IA Loop benchmark - AI agent simulation with complex state management
#include "common.h"
#include <cmath>
#include <algorithm>
#include <functional>
#include <vector>
// =============================================================================
// IA_LOOP BENCHMARK - DATA STRUCTURES
// =============================================================================
// Velocity table for entity movement
struct Velocity {
uint32_t entity_id;
float x;
float y;
float z;
};
SPACETIMEDB_STRUCT(Velocity, entity_id, x, y, z)
SPACETIMEDB_TABLE(Velocity, velocity, Public)
FIELD_PrimaryKey(velocity, entity_id)
// Position table with extended velocity fields
struct Position {
uint32_t entity_id;
float x;
float y;
float z;
float vx;
float vy;
float vz;
};
SPACETIMEDB_STRUCT(Position, entity_id, x, y, z, vx, vy, vz)
SPACETIMEDB_TABLE(Position, position, Public)
FIELD_PrimaryKey(position, entity_id)
// Agent action enumeration
SPACETIMEDB_ENUM(AgentAction, Inactive, Idle, Evading, Investigating, Retreating, Fighting)
// AI agent state management
struct GameEnemyAiAgentState {
uint64_t entity_id;
std::vector<uint64_t> last_move_timestamps;
uint64_t next_action_timestamp;
AgentAction action;
};
SPACETIMEDB_STRUCT(GameEnemyAiAgentState, entity_id, last_move_timestamps, next_action_timestamp, action)
SPACETIMEDB_TABLE(GameEnemyAiAgentState, game_enemy_ai_agent_state, Public)
FIELD_PrimaryKey(game_enemy_ai_agent_state, entity_id)
// Targetable state for spatial queries
struct GameTargetableState {
uint64_t entity_id;
int64_t quad;
};
SPACETIMEDB_STRUCT(GameTargetableState, entity_id, quad)
SPACETIMEDB_TABLE(GameTargetableState, game_targetable_state, Public)
FIELD_PrimaryKey(game_targetable_state, entity_id)
// Live targetable state with quad indexing
struct GameLiveTargetableState {
uint64_t entity_id;
int64_t quad;
};
SPACETIMEDB_STRUCT(GameLiveTargetableState, entity_id, quad)
SPACETIMEDB_TABLE(GameLiveTargetableState, game_live_targetable_state, Public)
FIELD_Unique(game_live_targetable_state, entity_id)
FIELD_Index(game_live_targetable_state, quad)
// Mobile entity state with spatial indexing
struct GameMobileEntityState {
uint64_t entity_id;
int32_t location_x;
int32_t location_y;
uint64_t timestamp;
};
SPACETIMEDB_STRUCT(GameMobileEntityState, entity_id, location_x, location_y, timestamp)
SPACETIMEDB_TABLE(GameMobileEntityState, game_mobile_entity_state, Public)
FIELD_PrimaryKey(game_mobile_entity_state, entity_id)
FIELD_Index(game_mobile_entity_state, location_x)
// Enemy state for herd management
struct GameEnemyState {
uint64_t entity_id;
int32_t herd_id;
};
SPACETIMEDB_STRUCT(GameEnemyState, entity_id, herd_id)
SPACETIMEDB_TABLE(GameEnemyState, game_enemy_state, Public)
FIELD_PrimaryKey(game_enemy_state, entity_id)
// Small hex tile coordinate structure
struct SmallHexTile {
int32_t x;
int32_t z;
uint32_t dimension;
};
SPACETIMEDB_STRUCT(SmallHexTile, x, z, dimension)
// Herd cache for AI behavior
struct GameHerdCache {
int32_t id;
uint32_t dimension_id;
int32_t current_population;
SmallHexTile location;
int32_t max_population;
float spawn_eagerness;
int32_t roaming_distance;
};
SPACETIMEDB_STRUCT(GameHerdCache, id, dimension_id, current_population, location, max_population, spawn_eagerness, roaming_distance)
SPACETIMEDB_TABLE(GameHerdCache, game_herd_cache, Public)
FIELD_PrimaryKey(game_herd_cache, id)
// =============================================================================
// IA_LOOP BENCHMARK - HELPER FUNCTIONS
// =============================================================================
// Simplified moment calculation - always returns 1 as per original implementations
inline uint64_t moment_milliseconds() {
return 1;
}
// Simple hash calculation for quad values
inline uint64_t calculate_hash(int64_t value) {
return static_cast<uint64_t>(std::hash<int64_t>{}(value));
}
// =============================================================================
// IA_LOOP BENCHMARK - POSITION AND VELOCITY OPERATIONS
// =============================================================================
// Bulk insert position entries
SPACETIMEDB_REDUCER(insert_bulk_position, ReducerContext& ctx, uint32_t count) {
for (uint32_t id = 0; id < count; ++id) {
float x = static_cast<float>(id);
float y = static_cast<float>(id + 5);
float z = static_cast<float>(id * 5);
Position new_position = {
id, x, y, z,
x + 10.0f, y + 20.0f, z + 30.0f // vx, vy, vz
};
ctx.db[position].insert(new_position);
}
LOG_INFO("INSERT POSITION: " + std::to_string(count));
return Ok();
}
// Bulk insert velocity entries
SPACETIMEDB_REDUCER(insert_bulk_velocity, ReducerContext& ctx, uint32_t count) {
for (uint32_t id = 0; id < count; ++id) {
Velocity new_velocity = {
id,
static_cast<float>(id),
static_cast<float>(id + 5),
static_cast<float>(id * 5)
};
ctx.db[velocity].insert(new_velocity);
}
LOG_INFO("INSERT VELOCITY: " + std::to_string(count));
return Ok();
}
// Update all positions using their internal velocity
SPACETIMEDB_REDUCER(update_position_all, ReducerContext& ctx, uint32_t expected) {
uint32_t count = 0;
for (auto pos : ctx.db[position]) {
pos.x += pos.vx;
pos.y += pos.vy;
pos.z += pos.vz;
auto _updated = ctx.db[position_entity_id].update(pos);
++count;
}
LOG_INFO("UPDATE POSITION ALL: " + std::to_string(expected) + ", processed: " + std::to_string(count));
return Ok();
}
// Update positions using separate velocity table
SPACETIMEDB_REDUCER(update_position_with_velocity, ReducerContext& ctx, uint32_t expected) {
uint32_t count = 0;
for (const auto& vel : ctx.db[velocity]) {
auto pos_opt = ctx.db[position_entity_id].find(vel.entity_id);
if (!pos_opt) {
continue;
}
auto pos = *pos_opt;
pos.x += vel.x;
pos.y += vel.y;
pos.z += vel.z;
auto _updated = ctx.db[position_entity_id].update(pos);
++count;
}
LOG_INFO("UPDATE POSITION BY VELOCITY: " + std::to_string(expected) + ", processed: " + std::to_string(count));
return Ok();
}
// =============================================================================
// IA_LOOP BENCHMARK - WORLD SETUP
// =============================================================================
// Insert complete game world state for specified number of players
SPACETIMEDB_REDUCER(insert_world, ReducerContext& ctx, uint64_t players) {
for (uint64_t i = 0; i < players; ++i) {
uint64_t next_action_timestamp = (i & 2) == 2 ?
moment_milliseconds() + 2000 : moment_milliseconds();
// Insert AI agent state
std::vector<uint64_t> move_timestamps = {i, 0, i * 2};
GameEnemyAiAgentState agent_state = {
i, move_timestamps, next_action_timestamp, AgentAction::Idle
};
ctx.db[game_enemy_ai_agent_state].insert(agent_state);
// Insert live targetable state
GameLiveTargetableState live_targetable = {
i, static_cast<int64_t>(i)
};
ctx.db[game_live_targetable_state].insert(live_targetable);
// Insert targetable state
GameTargetableState targetable = {
i, static_cast<int64_t>(i)
};
ctx.db[game_targetable_state].insert(targetable);
// Insert mobile entity state
GameMobileEntityState mobile_entity = {
i, static_cast<int32_t>(i), static_cast<int32_t>(i), next_action_timestamp
};
ctx.db[game_mobile_entity_state].insert(mobile_entity);
// Insert enemy state
GameEnemyState enemy = {
i, static_cast<int32_t>(i)
};
ctx.db[game_enemy_state].insert(enemy);
// Insert herd cache
SmallHexTile tile = {
static_cast<int32_t>(i),
static_cast<int32_t>(i),
static_cast<uint32_t>(i * 2)
};
GameHerdCache herd = {
static_cast<int32_t>(i),
static_cast<uint32_t>(i),
static_cast<int32_t>(i * 2),
tile,
static_cast<int32_t>(i * 4),
static_cast<float>(i),
static_cast<int32_t>(i)
};
ctx.db[game_herd_cache].insert(herd);
}
LOG_INFO("INSERT WORLD PLAYERS: " + std::to_string(players));
return Ok();
}
// =============================================================================
// IA_LOOP BENCHMARK - GAME LOGIC
// =============================================================================
// Get targetable entities near a specific quad
std::vector<GameTargetableState> get_targetables_near_quad(
ReducerContext& ctx, uint64_t entity_id, uint64_t num_players) {
std::vector<GameTargetableState> result;
result.reserve(4);
for (uint64_t id = entity_id; id < num_players; ++id) {
int64_t quad = static_cast<int64_t>(id);
for (const auto& t : ctx.db[game_live_targetable_state_quad].filter(quad)) {
auto targetable_opt = ctx.db[game_targetable_state_entity_id].find(t.entity_id);
if (!targetable_opt) {
LOG_PANIC("Identity not found");
return result;
}
result.push_back(*targetable_opt);
}
}
return result;
}
const std::size_t MAX_MOVE_TIMESTAMPS = 20;
// Move agent logic - updates agent state and related entities
void move_agent(ReducerContext& ctx, GameEnemyAiAgentState& agent,
const SmallHexTile& agent_coord, uint64_t current_time_ms) {
uint64_t entity_id = agent.entity_id;
// Update enemy state
auto enemy_opt = ctx.db[game_enemy_state_entity_id].find(entity_id);
if (!enemy_opt) {
LOG_PANIC("GameEnemyState Entity ID not found");
return;
}
auto enemy = *enemy_opt;
auto _updated = ctx.db[game_enemy_state_entity_id].update(enemy);
// Update agent timestamp
agent.next_action_timestamp = current_time_ms + 2000;
// Track movement timestamps
agent.last_move_timestamps.push_back(current_time_ms);
if (agent.last_move_timestamps.size() > MAX_MOVE_TIMESTAMPS) {
agent.last_move_timestamps.erase(agent.last_move_timestamps.begin());
}
// Update targetable state
auto targetable_opt = ctx.db[game_targetable_state_entity_id].find(entity_id);
if (!targetable_opt) {
LOG_PANIC("GameTargetableState Entity ID not found");
return;
}
auto targetable = *targetable_opt;
int64_t new_hash = static_cast<int64_t>(calculate_hash(targetable.quad));
targetable.quad = new_hash;
_updated = ctx.db[game_targetable_state_entity_id].update(targetable);
// Update live targetable state if exists
auto live_targetable_opt = ctx.db[game_live_targetable_state_entity_id].find(entity_id);
if (live_targetable_opt) {
GameLiveTargetableState live_targetable = {entity_id, new_hash};
ctx.db[game_live_targetable_state_entity_id].update(live_targetable);
}
// Update mobile entity state
auto mobile_entity_opt = ctx.db[game_mobile_entity_state_entity_id].find(entity_id);
if (!mobile_entity_opt) {
LOG_PANIC("GameMobileEntityState Entity ID not found");
return;
}
auto mobile_entity = *mobile_entity_opt;
mobile_entity.location_x += 1;
mobile_entity.location_y += 1;
mobile_entity.timestamp = agent.next_action_timestamp;
// Update agent state and mobile entity
_updated = ctx.db[game_enemy_ai_agent_state_entity_id].update(agent);
_updated = ctx.db[game_mobile_entity_state_entity_id].update(mobile_entity);
}
// Main agent loop processing
void agent_loop(ReducerContext& ctx, GameEnemyAiAgentState& agent,
const GameTargetableState& agent_targetable,
const std::vector<GameTargetableState>& surrounding_agents,
uint64_t current_time_ms) {
uint64_t entity_id = agent.entity_id;
auto coordinates_opt = ctx.db[game_mobile_entity_state_entity_id].find(entity_id);
if (!coordinates_opt) {
LOG_PANIC("GameMobileEntityState Entity ID not found");
return;
}
auto agent_entity_opt = ctx.db[game_enemy_state_entity_id].find(entity_id);
if (!agent_entity_opt) {
LOG_PANIC("GameEnemyState Entity ID not found");
return;
}
const auto& agent_entity = *agent_entity_opt;
auto agent_herd_opt = ctx.db[game_herd_cache_id].find(agent_entity.herd_id);
if (!agent_herd_opt) {
LOG_PANIC("GameHerdCache Entity ID not found");
return;
}
const auto& agent_herd = *agent_herd_opt;
SmallHexTile agent_herd_coordinates = agent_herd.location;
move_agent(ctx, agent, agent_herd_coordinates, current_time_ms);
}
// Main game loop for enemy AI processing
SPACETIMEDB_REDUCER(game_loop_enemy_ia, ReducerContext& ctx, uint64_t players) {
uint32_t count = 0;
uint64_t current_time_ms = moment_milliseconds();
for (auto agent : ctx.db[game_enemy_ai_agent_state]) {
auto agent_targetable_opt = ctx.db[game_targetable_state_entity_id].find(agent.entity_id);
if (!agent_targetable_opt) {
return Err("No TargetableState for AgentState entity");
}
const auto& agent_targetable = *agent_targetable_opt;
auto surrounding_agents = get_targetables_near_quad(ctx, agent_targetable.entity_id, players);
agent.action = AgentAction::Fighting;
agent_loop(ctx, agent, agent_targetable, surrounding_agents, current_time_ms);
++count;
}
LOG_INFO("ENEMY IA LOOP PLAYERS: " + std::to_string(players) + ", processed: " + std::to_string(count));
return Ok();
}
// =============================================================================
// IA_LOOP BENCHMARK - GAME SIMULATION ENTRY POINTS
// =============================================================================
// Initialize the IA loop game simulation with test data
SPACETIMEDB_REDUCER(init_game_ia_loop, ReducerContext& ctx, uint32_t initial_load) {
Load load(initial_load);
auto bulk_position_res = insert_bulk_position(ctx, load.biggest_table);
if (bulk_position_res.is_err()) {
return bulk_position_res;
}
auto bulk_velocity_res = insert_bulk_velocity(ctx, load.big_table);
if (bulk_velocity_res.is_err()) {
return bulk_velocity_res;
}
auto update_position_all_res = update_position_all(ctx, load.biggest_table);
if (update_position_all_res.is_err()) {
return update_position_all_res;
}
auto update_position_with_velocity_res = update_position_with_velocity(ctx, load.big_table);
if (update_position_with_velocity_res.is_err()) {
return update_position_with_velocity_res;
}
auto insert_world_res = insert_world(ctx, static_cast<uint64_t>(load.num_players));
if (insert_world_res.is_err()) {
return insert_world_res;
}
return Ok();
}
// Run the IA loop game simulation benchmark
SPACETIMEDB_REDUCER(run_game_ia_loop, ReducerContext& ctx, uint32_t initial_load) {
Load load(initial_load);
auto game_loop_enemy_ia_res = game_loop_enemy_ia(ctx, static_cast<uint64_t>(load.num_players));
if (game_loop_enemy_ia_res.is_err()) {
return game_loop_enemy_ia_res;
}
return Ok();
}