feat: Enhance rendering and crash recovery configurations

- Added new rendering budget configurations including VRAM limits and texture dimensions in seed_runtime.json and seed_runtime_opengl.json.
- Introduced crash recovery parameters such as heartbeat timeouts and memory limits in the configuration files.
- Updated cube logic to utilize new camera and control settings from the configuration.
- Modified bgfx graphics backend to respect new rendering budget limits and handle texture loading accordingly.
- Implemented crash recovery service enhancements to utilize new configuration parameters for better resource management.
- Added unit tests to validate the integration of new rendering budgets and crash recovery configurations.
This commit is contained in:
2026-01-08 18:11:21 +00:00
parent 40740782d0
commit 91a6d02d1f
17 changed files with 499 additions and 59 deletions

View File

@@ -73,6 +73,28 @@
},
"scene": {
"rotation_speed": 0.9,
"cube_mesh": {
"path": "models/cube.stl",
"double_sided": true
},
"camera": {
"position": [0.0, 1.6, 10.0],
"fov": 0.78,
"near": 0.1,
"far": 50.0
},
"controls": {
"move_speed": 16.0,
"fly_speed": 3.0,
"jump_speed": 5.5,
"gravity": -12.0,
"max_fall_speed": -20.0,
"mouse_sensitivity": 0.0025,
"gamepad_look_speed": 2.5,
"stick_deadzone": 0.2,
"max_pitch_degrees": 85.0,
"move_forward_uses_pitch": true
},
"room": {
"half_size": 15.0,
"wall_thickness": 0.5,
@@ -163,6 +185,22 @@
"pbr_metallic": 0.08
}
},
"budgets": {
"vram_mb": 512,
"max_texture_dim": 4096,
"gui_text_cache_entries": 256,
"gui_svg_cache_entries": 64
},
"crash_recovery": {
"heartbeat_timeout_ms": 5000,
"heartbeat_poll_interval_ms": 200,
"memory_limit_mb": 1024,
"gpu_hang_frame_time_multiplier": 10.0,
"max_consecutive_gpu_timeouts": 5,
"max_lua_failures": 3,
"max_file_format_errors": 2,
"max_memory_warnings": 3
},
"gui": {
"font": {
"use_freetype": true,

View File

@@ -70,6 +70,65 @@
"gamepad_axis_action_threshold": 0.5
}
},
"scene": {
"rotation_speed": 0.9,
"cube_mesh": {
"path": "models/cube.stl",
"double_sided": true
},
"camera": {
"position": [0.0, 1.6, 10.0],
"fov": 0.78,
"near": 0.1,
"far": 50.0
},
"controls": {
"move_speed": 16.0,
"fly_speed": 3.0,
"jump_speed": 5.5,
"gravity": -12.0,
"max_fall_speed": -20.0,
"mouse_sensitivity": 0.0025,
"gamepad_look_speed": 2.5,
"stick_deadzone": 0.2,
"max_pitch_degrees": 85.0,
"move_forward_uses_pitch": true
},
"room": {
"half_size": 15.0,
"wall_thickness": 0.5,
"wall_height": 4.0,
"floor_half_thickness": 0.3,
"floor_top": 0.0,
"floor_subdivisions": 20,
"floor_color": [1.0, 1.0, 1.0],
"wall_color": [1.0, 1.0, 1.0],
"ceiling_color": [1.0, 1.0, 1.0]
},
"physics_cube": {
"enabled": true,
"half_extents": [1.5, 1.5, 1.5],
"mass": 1.0,
"color": [0.92, 0.34, 0.28],
"kick_strength": 6.0,
"gravity": [0.0, -9.8, 0.0],
"max_sub_steps": 10
},
"spinning_cube": {
"enabled": true,
"scale": 1.5,
"height": 5.0,
"color": [0.75, 0.45, 0.25]
},
"lanterns": {
"enabled": true,
"height": 8.0,
"size": 0.2,
"color": [1.0, 0.9, 0.6],
"corner_offset": 2.0,
"wall_offset": 2.0
}
},
"rendering": {
"bgfx": {
"renderer": "opengl"
@@ -125,6 +184,22 @@
"pbr_metallic": 0.08
}
},
"budgets": {
"vram_mb": 512,
"max_texture_dim": 4096,
"gui_text_cache_entries": 256,
"gui_svg_cache_entries": 64
},
"crash_recovery": {
"heartbeat_timeout_ms": 5000,
"heartbeat_poll_interval_ms": 200,
"memory_limit_mb": 1024,
"gpu_hang_frame_time_multiplier": 10.0,
"max_consecutive_gpu_timeouts": 5,
"max_lua_failures": 3,
"max_file_format_errors": 2,
"max_memory_warnings": 3
},
"gui": {
"font": {
"use_freetype": true,

View File

@@ -2,8 +2,19 @@ local scene_framework = require("scene_framework")
local math3d = require("math3d")
local config_resolver = require("config_resolver")
local resolve_number = scene_framework.resolve_number
local resolve_boolean = scene_framework.resolve_boolean
local resolve_string = scene_framework.resolve_string
local resolve_table = scene_framework.resolve_table
local resolve_vec3 = scene_framework.resolve_vec3
local scene_config = resolve_table(config_resolver.resolve_scene(config))
local cube_mesh_config = resolve_table(scene_config.cube_mesh)
local cube_mesh_path = resolve_string(cube_mesh_config.path, "models/cube.stl")
local cube_mesh_double_sided = resolve_boolean(cube_mesh_config.double_sided, true)
local cube_mesh_info = {
path = "models/cube.stl",
path = cube_mesh_path,
loaded = false,
vertex_count = 0,
index_count = 0,
@@ -63,7 +74,11 @@ local function load_cube_mesh()
cube_vertices = mesh.vertices
cube_indices = mesh.indices
cube_indices_double_sided = build_double_sided_indices(cube_indices)
if cube_mesh_double_sided then
cube_indices_double_sided = build_double_sided_indices(cube_indices)
else
cube_indices_double_sided = {}
end
cube_mesh_info.loaded = true
cube_mesh_info.vertex_count = #mesh.vertices
cube_mesh_info.index_count = #mesh.indices
@@ -108,11 +123,6 @@ start_music()
local Gui = require("gui")
local string_format = string.format
local resolve_number = scene_framework.resolve_number
local resolve_boolean = scene_framework.resolve_boolean
local resolve_table = scene_framework.resolve_table
local resolve_vec3 = scene_framework.resolve_vec3
local function resolve_positive_int(value, fallback)
local candidate = resolve_number(value, fallback)
local rounded = math.floor(candidate)
@@ -275,26 +285,37 @@ if cube_mesh_info.loaded then
end
end
local camera_config = resolve_table(scene_config.camera)
local controls_config = resolve_table(scene_config.controls)
local max_pitch_degrees = resolve_number(controls_config.max_pitch_degrees, nil)
local max_pitch = resolve_number(controls_config.max_pitch, nil)
if max_pitch_degrees then
max_pitch = math.rad(max_pitch_degrees)
end
if not max_pitch then
max_pitch = math.rad(85.0)
end
local camera = {
position = {0.0, 0.0, 5.0},
yaw = math.pi, -- Face toward -Z (center of room)
pitch = 0.0,
fov = 0.78,
near = 0.1,
far = 50.0,
yaw = resolve_number(camera_config.yaw, math.pi),
pitch = resolve_number(camera_config.pitch, 0.0),
fov = resolve_number(camera_config.fov, 0.78),
near = resolve_number(camera_config.near, 0.1),
far = resolve_number(camera_config.far, 50.0),
}
local controls = {
move_speed = 16.0,
fly_speed = 3.0,
jump_speed = 5.5,
gravity = -12.0,
max_fall_speed = -20.0,
mouse_sensitivity = 0.0025,
gamepad_look_speed = 2.5,
stick_deadzone = 0.2,
max_pitch = math.rad(85.0),
move_forward_uses_pitch = true,
move_speed = resolve_number(controls_config.move_speed, 16.0),
fly_speed = resolve_number(controls_config.fly_speed, 3.0),
jump_speed = resolve_number(controls_config.jump_speed, 5.5),
gravity = resolve_number(controls_config.gravity, -12.0),
max_fall_speed = resolve_number(controls_config.max_fall_speed, -20.0),
mouse_sensitivity = resolve_number(controls_config.mouse_sensitivity, 0.0025),
gamepad_look_speed = resolve_number(controls_config.gamepad_look_speed, 2.5),
stick_deadzone = resolve_number(controls_config.stick_deadzone, 0.2),
max_pitch = max_pitch,
move_forward_uses_pitch = resolve_boolean(controls_config.move_forward_uses_pitch, true),
}
local last_frame_time = nil
@@ -310,7 +331,6 @@ local function get_time_seconds()
end
local movement_log_cooldown = 0.0
local world_up = {0.0, 1.0, 0.0}
local scene_config = resolve_table(config_resolver.resolve_scene(config))
local room_config = resolve_table(scene_config.room)
local lantern_config = resolve_table(scene_config.lanterns)
local physics_config = resolve_table(scene_config.physics_cube)
@@ -385,9 +405,11 @@ local physics_state = {
gravity = physics_gravity,
}
camera.position[1] = 0.0
camera.position[2] = room.floor_top + player_state.eye_height
camera.position[3] = 10.0
camera.position = resolve_vec3(camera_config.position, {
0.0,
room.floor_top + player_state.eye_height,
10.0,
})
local function clamp(value, minValue, maxValue)
if value < minValue then

View File

@@ -194,7 +194,8 @@ void ServiceBasedApp::RegisterServices() {
// Crash recovery service (needed early for crash detection)
registry_.RegisterService<services::ICrashRecoveryService, services::impl::CrashRecoveryService>(
registry_.GetService<services::ILogger>());
registry_.GetService<services::ILogger>(),
runtimeConfig_.crashRecovery);
// Lifecycle service
registry_.RegisterService<services::ILifecycleService, services::impl::LifecycleService>(

View File

@@ -18,6 +18,7 @@
#include <glm/gtc/matrix_inverse.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iterator>
#include <limits>
#include <optional>
#include <string>
#include <stdexcept>
@@ -284,6 +285,16 @@ BgfxGraphicsBackend::BgfxGraphicsBackend(std::shared_ptr<IConfigService> configS
"configService=" + std::string(configService_ ? "set" : "null") +
", platformService=" + std::string(platformService_ ? "set" : "null"));
}
if (configService_) {
const auto& budgets = configService_->GetRenderBudgetConfig();
textureMemoryTracker_.SetMaxBytes(budgets.vramMB * 1024 * 1024);
maxTextureDim_ = budgets.maxTextureDim;
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "BgfxGraphicsBackend",
"vramMB=" + std::to_string(budgets.vramMB) +
", maxTextureDim=" + std::to_string(maxTextureDim_));
}
}
vertexLayout_.begin()
.add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
.add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float)
@@ -735,10 +746,14 @@ bgfx::ShaderHandle BgfxGraphicsBackend::CreateShader(const std::string& label,
}
bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string& path,
uint64_t samplerFlags) const {
uint64_t samplerFlags,
size_t* outSizeBytes) const {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "LoadTextureFromFile", "path=" + path);
}
if (outSizeBytes) {
*outSizeBytes = 0;
}
if (!HasProcessedFrame()) {
if (logger_) {
logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: Attempted to load texture BEFORE first "
@@ -762,6 +777,18 @@ bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string&
return BGFX_INVALID_HANDLE;
}
if (maxTextureDim_ > 0) {
if (width > static_cast<int>(maxTextureDim_) || height > static_cast<int>(maxTextureDim_)) {
if (logger_) {
logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: texture " + path +
" size (" + std::to_string(width) + "x" + std::to_string(height) +
") exceeds config max texture dim (" + std::to_string(maxTextureDim_) + ")");
}
stbi_image_free(pixels);
return BGFX_INVALID_HANDLE;
}
}
// Validate texture dimensions against GPU capabilities
const bgfx::Caps* caps = bgfx::getCaps();
if (caps) {
@@ -777,7 +804,16 @@ bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string&
}
}
const uint32_t size = static_cast<uint32_t>(width * height * 4);
const size_t size = static_cast<size_t>(width) * static_cast<size_t>(height) * 4u;
if (size > std::numeric_limits<uint32_t>::max()) {
if (logger_) {
logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: texture " + path +
" exceeds supported size for upload (" + std::to_string(size) + " bytes)");
}
stbi_image_free(pixels);
return BGFX_INVALID_HANDLE;
}
const uint32_t sizeBytes = static_cast<uint32_t>(size);
// Check memory budget before allocation
if (!textureMemoryTracker_.CanAllocate(size)) {
@@ -791,7 +827,7 @@ bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string&
return BGFX_INVALID_HANDLE;
}
const bgfx::Memory* mem = bgfx::copy(pixels, size);
const bgfx::Memory* mem = bgfx::copy(pixels, sizeBytes);
stbi_image_free(pixels);
// Validate bgfx::copy() succeeded
@@ -830,6 +866,10 @@ bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string&
", memoryMB=" + std::to_string(size / 1024 / 1024));
}
if (outSizeBytes) {
*outSizeBytes = size;
}
return handle;
}
@@ -955,23 +995,34 @@ GraphicsPipelineHandle BgfxGraphicsBackend::CreatePipeline(GraphicsDeviceHandle
}
// Try to load texture from file
binding.texture = LoadTextureFromFile(binding.sourcePath, samplerFlags);
size_t textureSizeBytes = 0;
binding.texture = LoadTextureFromFile(binding.sourcePath, samplerFlags, &textureSizeBytes);
if (bgfx::isValid(binding.texture)) {
// Estimate texture memory size (assume RGBA8 format, no mipmaps for now)
// In production, should query actual texture info from bgfx
// For now, estimate based on typical 2048x2048 textures
binding.memorySizeBytes = 2048 * 2048 * 4; // Conservative estimate
textureMemoryTracker_.Allocate(binding.memorySizeBytes);
binding.memorySizeBytes = textureSizeBytes;
if (binding.memorySizeBytes > 0) {
textureMemoryTracker_.Allocate(binding.memorySizeBytes);
}
} else {
if (logger_) {
logger_->Warn("BgfxGraphicsBackend::CreatePipeline: texture load failed for " +
binding.sourcePath + ", creating fallback texture");
}
// Use fallback magenta texture (1x1)
binding.texture = CreateSolidTexture(0xff00ffff, samplerFlags);
if (bgfx::isValid(binding.texture)) {
binding.memorySizeBytes = 1 * 1 * 4; // 1x1 RGBA8
textureMemoryTracker_.Allocate(binding.memorySizeBytes);
binding.memorySizeBytes = 1 * 1 * 4; // 1x1 RGBA8
if (!textureMemoryTracker_.CanAllocate(binding.memorySizeBytes)) {
if (logger_) {
logger_->Warn("BgfxGraphicsBackend::CreatePipeline: budget prevents fallback texture for " +
binding.sourcePath);
}
binding.texture = BGFX_INVALID_HANDLE;
binding.memorySizeBytes = 0;
} else {
binding.texture = CreateSolidTexture(0xff00ffff, samplerFlags);
if (bgfx::isValid(binding.texture)) {
textureMemoryTracker_.Allocate(binding.memorySizeBytes);
} else {
binding.memorySizeBytes = 0;
}
}
}

View File

@@ -61,6 +61,9 @@ private:
TextureMemoryTracker() = default;
bool CanAllocate(size_t bytes) const {
if (maxBytes_ == 0) {
return true;
}
return (totalBytes_ + bytes) <= maxBytes_;
}
@@ -78,7 +81,7 @@ private:
size_t GetUsedBytes() const { return totalBytes_; }
size_t GetMaxBytes() const { return maxBytes_; }
size_t GetAvailableBytes() const { return maxBytes_ - totalBytes_; }
size_t GetAvailableBytes() const { return maxBytes_ == 0 ? 0 : maxBytes_ - totalBytes_; }
void SetMaxBytes(size_t max) { maxBytes_ = max; }
@@ -140,7 +143,9 @@ private:
bgfx::ShaderHandle CreateShader(const std::string& label,
const std::string& source,
bool isVertex) const;
bgfx::TextureHandle LoadTextureFromFile(const std::string& path, uint64_t samplerFlags) const;
bgfx::TextureHandle LoadTextureFromFile(const std::string& path,
uint64_t samplerFlags,
size_t* outSizeBytes = nullptr) const;
void InitializeUniforms();
void DestroyUniforms();
void ApplyMaterialXUniforms(const std::array<float, 16>& modelMatrix);
@@ -168,6 +173,7 @@ private:
bgfx::PlatformData platformData_{};
bool loggedInitFailureDiagnostics_ = false;
mutable TextureMemoryTracker textureMemoryTracker_{};
uint32_t maxTextureDim_ = 0;
};
} // namespace sdl3cpp::services::impl

View File

@@ -125,6 +125,18 @@ BgfxGuiService::BgfxGuiService(std::shared_ptr<IConfigService> configService,
"configService=" + std::string(configService_ ? "set" : "null") +
", pipelineCompiler=" + std::string(pipelineCompiler_ ? "set" : "null"));
}
if (configService_) {
const auto& budgets = configService_->GetRenderBudgetConfig();
maxTextCacheEntries_ = budgets.guiTextCacheEntries;
maxSvgCacheEntries_ = budgets.guiSvgCacheEntries;
maxTextureDim_ = budgets.maxTextureDim;
if (logger_) {
logger_->Trace("BgfxGuiService", "BgfxGuiService",
"guiTextCacheEntries=" + std::to_string(maxTextCacheEntries_) +
", guiSvgCacheEntries=" + std::to_string(maxSvgCacheEntries_) +
", maxTextureDim=" + std::to_string(maxTextureDim_));
}
}
}
BgfxGuiService::~BgfxGuiService() {
@@ -967,6 +979,22 @@ bgfx::TextureHandle BgfxGuiService::CreateTexture(const uint8_t* rgba,
if (!rgba || width == 0 || height == 0) {
return BGFX_INVALID_HANDLE;
}
uint32_t maxDim = maxTextureDim_;
if (const bgfx::Caps* caps = bgfx::getCaps()) {
if (caps->limits.maxTextureSize > 0) {
maxDim = (maxDim == 0)
? caps->limits.maxTextureSize
: std::min<uint32_t>(maxDim, caps->limits.maxTextureSize);
}
}
if (maxDim > 0 && (width > maxDim || height > maxDim)) {
if (logger_) {
logger_->Error("BgfxGuiService::CreateTexture: texture size (" +
std::to_string(width) + "x" + std::to_string(height) +
") exceeds max texture dim (" + std::to_string(maxDim) + ")");
}
return BGFX_INVALID_HANDLE;
}
const uint32_t size = width * height * 4;
const bgfx::Memory* mem = bgfx::copy(rgba, size);
return bgfx::createTexture2D(static_cast<uint16_t>(width),

View File

@@ -179,6 +179,7 @@ private:
uint64_t frameIndex_ = 0;
size_t maxTextCacheEntries_ = 256;
size_t maxSvgCacheEntries_ = 64;
uint32_t maxTextureDim_ = 0;
};
} // namespace sdl3cpp::services::impl

View File

@@ -35,8 +35,8 @@ int64_t GetSteadyClockNs() {
// Static instance for signal handler
CrashRecoveryService* CrashRecoveryService::instance_ = nullptr;
CrashRecoveryService::CrashRecoveryService(std::shared_ptr<ILogger> logger)
: logger_(logger)
CrashRecoveryService::CrashRecoveryService(std::shared_ptr<ILogger> logger, CrashRecoveryConfig config)
: logger_(std::move(logger))
, crashDetected_(false)
, lastSignal_(0)
, lastHeartbeatNs_(0)
@@ -48,10 +48,19 @@ CrashRecoveryService::CrashRecoveryService(std::shared_ptr<ILogger> logger)
, fileFormatErrors_(0)
, memoryWarnings_(0)
, lastHealthCheck_(std::chrono::steady_clock::now())
, signalHandlersInstalled_(false) {
logger_->Trace("CrashRecoveryService", "CrashRecoveryService",
"logger=" + std::string(logger_ ? "set" : "null"),
"Created");
, signalHandlersInstalled_(false)
, config_(std::move(config)) {
heartbeatTimeout_ = std::chrono::milliseconds(config_.heartbeatTimeoutMs);
heartbeatPollInterval_ = std::chrono::milliseconds(config_.heartbeatPollIntervalMs);
memoryLimitBytes_ = config_.memoryLimitMB * 1024 * 1024;
if (logger_) {
logger_->Trace("CrashRecoveryService", "CrashRecoveryService",
"logger=" + std::string(logger_ ? "set" : "null") +
", heartbeatTimeoutMs=" + std::to_string(config_.heartbeatTimeoutMs) +
", heartbeatPollIntervalMs=" + std::to_string(config_.heartbeatPollIntervalMs) +
", memoryLimitMB=" + std::to_string(config_.memoryLimitMB));
}
}
CrashRecoveryService::~CrashRecoveryService() {
@@ -69,8 +78,17 @@ void CrashRecoveryService::Initialize() {
heartbeatSeen_ = false;
crashReport_.clear();
if (!heartbeatMonitorRunning_.exchange(true)) {
heartbeatMonitorThread_ = std::thread(&CrashRecoveryService::MonitorHeartbeats, this);
const bool heartbeatEnabled = config_.heartbeatTimeoutMs > 0 && config_.heartbeatPollIntervalMs > 0;
if (heartbeatEnabled) {
if (!heartbeatMonitorRunning_.exchange(true)) {
heartbeatMonitorThread_ = std::thread(&CrashRecoveryService::MonitorHeartbeats, this);
}
} else {
heartbeatMonitorRunning_ = false;
if (logger_) {
logger_->Trace("CrashRecoveryService", "Initialize",
"heartbeatEnabled=false", "Heartbeat monitor disabled via config");
}
}
logger_->Info("CrashRecoveryService::Initialize: Crash recovery service initialized");
@@ -370,10 +388,16 @@ bool CrashRecoveryService::CheckGpuHealth(double lastFrameTime, double expectedF
", expectedFrameTime=" + std::to_string(expectedFrameTime));
UpdateHealthMetrics();
if (config_.gpuHangFrameTimeMultiplier <= 0.0 || config_.maxConsecutiveGpuTimeouts == 0) {
consecutiveFrameTimeouts_ = 0;
lastSuccessfulFrameTime_ = lastFrameTime;
return true;
}
// Check for GPU hangs - if we haven't had a successful frame in too long
if (lastFrameTime > expectedFrameTime * 10.0) { // 10x expected frame time
if (lastFrameTime > expectedFrameTime * config_.gpuHangFrameTimeMultiplier) {
consecutiveFrameTimeouts_++;
if (consecutiveFrameTimeouts_ > 5) { // 5 consecutive timeouts
if (consecutiveFrameTimeouts_ > config_.maxConsecutiveGpuTimeouts) {
std::lock_guard<std::mutex> lock(crashMutex_);
crashDetected_ = true;
crashReport_ += "\nGPU HANG DETECTED: No successful frames for " +
@@ -406,7 +430,7 @@ bool CrashRecoveryService::ValidateLuaExecution(bool scriptResult, const std::st
logger_->Error("CrashRecoveryService::ValidateLuaExecution: Lua script '" +
scriptName + "' failed to execute");
if (luaExecutionFailures_ > 3) { // Multiple Lua failures indicate a serious issue
if (config_.maxLuaFailures > 0 && luaExecutionFailures_ > config_.maxLuaFailures) {
crashDetected_ = true;
crashReport_ += "CRITICAL: Multiple Lua execution failures detected\n";
}
@@ -442,7 +466,7 @@ bool CrashRecoveryService::ValidateFileFormat(const std::string& filePath, const
logger_->Warn("CrashRecoveryService::ValidateFileFormat: Invalid format for " +
filePath + " - expected " + expectedFormat + ", got " + extension);
if (fileFormatErrors_ > 2) { // Multiple format errors
if (config_.maxFileFormatErrors > 0 && fileFormatErrors_ > config_.maxFileFormatErrors) {
crashDetected_ = true;
crashReport_ += "CRITICAL: Multiple file format errors detected\n";
}
@@ -476,13 +500,15 @@ bool CrashRecoveryService::CheckMemoryHealth() {
logger_->Trace("CrashRecoveryService", "CheckMemoryHealth");
UpdateHealthMetrics();
if (memoryLimitBytes_ == 0) {
return true;
}
size_t currentMemory = GetCurrentMemoryUsage();
// Basic memory usage check (this is a simplified version)
// In a real implementation, you'd track memory growth over time
const size_t MAX_MEMORY_MB = 1024; // 1GB limit for this example
if (currentMemory > MAX_MEMORY_MB * 1024 * 1024) {
if (currentMemory > memoryLimitBytes_) {
memoryWarnings_++;
std::lock_guard<std::mutex> lock(crashMutex_);
crashReport_ += "\nHIGH MEMORY USAGE: " + std::to_string(currentMemory / (1024*1024)) + " MB\n";
@@ -490,7 +516,7 @@ bool CrashRecoveryService::CheckMemoryHealth() {
logger_->Warn("CrashRecoveryService::CheckMemoryHealth: High memory usage detected: " +
std::to_string(currentMemory / (1024*1024)) + " MB");
if (memoryWarnings_ > 3) {
if (config_.maxMemoryWarnings > 0 && memoryWarnings_ > config_.maxMemoryWarnings) {
crashDetected_ = true;
crashReport_ += "CRITICAL: Persistent high memory usage detected\n";
return false;

View File

@@ -1,5 +1,6 @@
#pragma once
#include "../interfaces/config_types.hpp"
#include "../interfaces/i_crash_recovery_service.hpp"
#include "../interfaces/i_logger.hpp"
#include <atomic>
@@ -21,7 +22,7 @@ namespace sdl3cpp::services::impl {
*/
class CrashRecoveryService : public ICrashRecoveryService {
public:
explicit CrashRecoveryService(std::shared_ptr<ILogger> logger);
CrashRecoveryService(std::shared_ptr<ILogger> logger, CrashRecoveryConfig config);
~CrashRecoveryService() override;
// ICrashRecoveryService interface
@@ -65,6 +66,8 @@ private:
std::chrono::milliseconds heartbeatPollInterval_{200};
std::string crashReport_;
mutable std::mutex crashMutex_;
CrashRecoveryConfig config_{};
size_t memoryLimitBytes_ = 0;
// Health monitoring state
std::atomic<double> lastSuccessfulFrameTime_;

View File

@@ -959,6 +959,79 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
config.guiOpacity = static_cast<float>(value.GetDouble());
}
auto readSizeT = [](const rapidjson::Value& parent,
const char* name,
const std::string& path,
size_t& target) {
if (!parent.HasMember(name)) {
return;
}
const auto& value = parent[name];
if (!value.IsNumber()) {
throw std::runtime_error("JSON member '" + path + "." + std::string(name) + "' must be a number");
}
const double rawValue = value.GetDouble();
if (rawValue < 0.0) {
throw std::runtime_error("JSON member '" + path + "." + std::string(name) + "' must be non-negative");
}
target = static_cast<size_t>(rawValue);
};
auto readUint32 = [](const rapidjson::Value& parent,
const char* name,
const std::string& path,
uint32_t& target) {
if (!parent.HasMember(name)) {
return;
}
const auto& value = parent[name];
if (!value.IsNumber()) {
throw std::runtime_error("JSON member '" + path + "." + std::string(name) + "' must be a number");
}
const double rawValue = value.GetDouble();
if (rawValue < 0.0) {
throw std::runtime_error("JSON member '" + path + "." + std::string(name) + "' must be non-negative");
}
target = static_cast<uint32_t>(rawValue);
};
const auto* budgetsValue = getObjectMember(document, "budgets", "budgets");
if (budgetsValue) {
readSizeT(*budgetsValue, "vram_mb", "budgets", config.budgets.vramMB);
readUint32(*budgetsValue, "max_texture_dim", "budgets", config.budgets.maxTextureDim);
readSizeT(*budgetsValue, "gui_text_cache_entries", "budgets", config.budgets.guiTextCacheEntries);
readSizeT(*budgetsValue, "gui_svg_cache_entries", "budgets", config.budgets.guiSvgCacheEntries);
}
const auto* crashRecoveryValue = getObjectMember(document, "crash_recovery", "crash_recovery");
if (crashRecoveryValue) {
readUint32(*crashRecoveryValue, "heartbeat_timeout_ms",
"crash_recovery", config.crashRecovery.heartbeatTimeoutMs);
readUint32(*crashRecoveryValue, "heartbeat_poll_interval_ms",
"crash_recovery", config.crashRecovery.heartbeatPollIntervalMs);
readSizeT(*crashRecoveryValue, "memory_limit_mb",
"crash_recovery", config.crashRecovery.memoryLimitMB);
readSizeT(*crashRecoveryValue, "max_consecutive_gpu_timeouts",
"crash_recovery", config.crashRecovery.maxConsecutiveGpuTimeouts);
readSizeT(*crashRecoveryValue, "max_lua_failures",
"crash_recovery", config.crashRecovery.maxLuaFailures);
readSizeT(*crashRecoveryValue, "max_file_format_errors",
"crash_recovery", config.crashRecovery.maxFileFormatErrors);
readSizeT(*crashRecoveryValue, "max_memory_warnings",
"crash_recovery", config.crashRecovery.maxMemoryWarnings);
if (crashRecoveryValue->HasMember("gpu_hang_frame_time_multiplier")) {
const auto& value = (*crashRecoveryValue)["gpu_hang_frame_time_multiplier"];
if (!value.IsNumber()) {
throw std::runtime_error("JSON member 'crash_recovery.gpu_hang_frame_time_multiplier' must be a number");
}
const double rawValue = value.GetDouble();
if (rawValue < 0.0) {
throw std::runtime_error("JSON member 'crash_recovery.gpu_hang_frame_time_multiplier' must be non-negative");
}
config.crashRecovery.gpuHangFrameTimeMultiplier = rawValue;
}
}
return config;
}
@@ -1091,6 +1164,33 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config,
renderingObject.AddMember("atmospherics", atmosphericsObject, allocator);
document.AddMember("rendering", renderingObject, allocator);
rapidjson::Value budgetsObject(rapidjson::kObjectType);
budgetsObject.AddMember("vram_mb", static_cast<uint64_t>(config.budgets.vramMB), allocator);
budgetsObject.AddMember("max_texture_dim", config.budgets.maxTextureDim, allocator);
budgetsObject.AddMember("gui_text_cache_entries",
static_cast<uint64_t>(config.budgets.guiTextCacheEntries),
allocator);
budgetsObject.AddMember("gui_svg_cache_entries",
static_cast<uint64_t>(config.budgets.guiSvgCacheEntries),
allocator);
document.AddMember("budgets", budgetsObject, allocator);
rapidjson::Value crashObject(rapidjson::kObjectType);
crashObject.AddMember("heartbeat_timeout_ms", config.crashRecovery.heartbeatTimeoutMs, allocator);
crashObject.AddMember("heartbeat_poll_interval_ms", config.crashRecovery.heartbeatPollIntervalMs, allocator);
crashObject.AddMember("memory_limit_mb", static_cast<uint64_t>(config.crashRecovery.memoryLimitMB), allocator);
crashObject.AddMember("gpu_hang_frame_time_multiplier",
config.crashRecovery.gpuHangFrameTimeMultiplier, allocator);
crashObject.AddMember("max_consecutive_gpu_timeouts",
static_cast<uint64_t>(config.crashRecovery.maxConsecutiveGpuTimeouts), allocator);
crashObject.AddMember("max_lua_failures",
static_cast<uint64_t>(config.crashRecovery.maxLuaFailures), allocator);
crashObject.AddMember("max_file_format_errors",
static_cast<uint64_t>(config.crashRecovery.maxFileFormatErrors), allocator);
crashObject.AddMember("max_memory_warnings",
static_cast<uint64_t>(config.crashRecovery.maxMemoryWarnings), allocator);
document.AddMember("crash_recovery", crashObject, allocator);
rapidjson::Value bindingsObject(rapidjson::kObjectType);
auto addBindingMember = [&](const char* name, const std::string& value) {
rapidjson::Value nameValue(name, allocator);

View File

@@ -111,6 +111,18 @@ public:
}
return config_.guiFont;
}
const RenderBudgetConfig& GetRenderBudgetConfig() const override {
if (logger_) {
logger_->Trace("JsonConfigService", "GetRenderBudgetConfig");
}
return config_.budgets;
}
const CrashRecoveryConfig& GetCrashRecoveryConfig() const override {
if (logger_) {
logger_->Trace("JsonConfigService", "GetCrashRecoveryConfig");
}
return config_.crashRecovery;
}
const std::string& GetConfigJson() const override {
if (logger_) {
logger_->Trace("JsonConfigService", "GetConfigJson");

View File

@@ -225,6 +225,33 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std
renderingObject.AddMember("atmospherics", atmosphericsObject, allocator);
document.AddMember("rendering", renderingObject, allocator);
rapidjson::Value budgetsObject(rapidjson::kObjectType);
budgetsObject.AddMember("vram_mb", static_cast<uint64_t>(config.budgets.vramMB), allocator);
budgetsObject.AddMember("max_texture_dim", config.budgets.maxTextureDim, allocator);
budgetsObject.AddMember("gui_text_cache_entries",
static_cast<uint64_t>(config.budgets.guiTextCacheEntries),
allocator);
budgetsObject.AddMember("gui_svg_cache_entries",
static_cast<uint64_t>(config.budgets.guiSvgCacheEntries),
allocator);
document.AddMember("budgets", budgetsObject, allocator);
rapidjson::Value crashObject(rapidjson::kObjectType);
crashObject.AddMember("heartbeat_timeout_ms", config.crashRecovery.heartbeatTimeoutMs, allocator);
crashObject.AddMember("heartbeat_poll_interval_ms", config.crashRecovery.heartbeatPollIntervalMs, allocator);
crashObject.AddMember("memory_limit_mb", static_cast<uint64_t>(config.crashRecovery.memoryLimitMB), allocator);
crashObject.AddMember("gpu_hang_frame_time_multiplier",
config.crashRecovery.gpuHangFrameTimeMultiplier, allocator);
crashObject.AddMember("max_consecutive_gpu_timeouts",
static_cast<uint64_t>(config.crashRecovery.maxConsecutiveGpuTimeouts), allocator);
crashObject.AddMember("max_lua_failures",
static_cast<uint64_t>(config.crashRecovery.maxLuaFailures), allocator);
crashObject.AddMember("max_file_format_errors",
static_cast<uint64_t>(config.crashRecovery.maxFileFormatErrors), allocator);
crashObject.AddMember("max_memory_warnings",
static_cast<uint64_t>(config.crashRecovery.maxMemoryWarnings), allocator);
document.AddMember("crash_recovery", crashObject, allocator);
rapidjson::Value guiObject(rapidjson::kObjectType);
rapidjson::Value fontObject(rapidjson::kObjectType);
fontObject.AddMember("use_freetype", config.guiFont.useFreeType, allocator);

View File

@@ -120,6 +120,30 @@ struct GuiFontConfig {
float fontSize = 18.0f;
};
/**
* @brief Resource budget and rendering limits.
*/
struct RenderBudgetConfig {
size_t vramMB = 512;
uint32_t maxTextureDim = 0;
size_t guiTextCacheEntries = 256;
size_t guiSvgCacheEntries = 64;
};
/**
* @brief Crash recovery tuning parameters.
*/
struct CrashRecoveryConfig {
uint32_t heartbeatTimeoutMs = 5000;
uint32_t heartbeatPollIntervalMs = 200;
size_t memoryLimitMB = 1024;
double gpuHangFrameTimeMultiplier = 10.0;
size_t maxConsecutiveGpuTimeouts = 5;
size_t maxLuaFailures = 3;
size_t maxFileFormatErrors = 2;
size_t maxMemoryWarnings = 3;
};
/**
* @brief Runtime configuration values used across services.
*/
@@ -135,8 +159,10 @@ struct RuntimeConfig {
BgfxConfig bgfx{};
MaterialXConfig materialX{};
std::vector<MaterialXMaterialConfig> materialXMaterials{};
RenderBudgetConfig budgets{};
GuiFontConfig guiFont{};
float guiOpacity = 1.0f;
CrashRecoveryConfig crashRecovery{};
};
} // namespace sdl3cpp::services

View File

@@ -85,6 +85,18 @@ public:
*/
virtual const GuiFontConfig& GetGuiFontConfig() const = 0;
/**
* @brief Get rendering budgets.
* @return Render budget configuration
*/
virtual const RenderBudgetConfig& GetRenderBudgetConfig() const = 0;
/**
* @brief Get crash recovery configuration.
* @return Crash recovery configuration
*/
virtual const CrashRecoveryConfig& GetCrashRecoveryConfig() const = 0;
/**
* @brief Get the full JSON configuration as a string.
*

View File

@@ -148,6 +148,8 @@ public:
return materialXMaterials_;
}
const sdl3cpp::services::GuiFontConfig& GetGuiFontConfig() const override { return guiFontConfig_; }
const sdl3cpp::services::RenderBudgetConfig& GetRenderBudgetConfig() const override { return budgets_; }
const sdl3cpp::services::CrashRecoveryConfig& GetCrashRecoveryConfig() const override { return crashRecovery_; }
const std::string& GetConfigJson() const override { return configJson_; }
void DisableFreeType() {
@@ -173,6 +175,8 @@ private:
sdl3cpp::services::MaterialXConfig materialXConfig_{};
std::vector<sdl3cpp::services::MaterialXMaterialConfig> materialXMaterials_{};
sdl3cpp::services::GuiFontConfig guiFontConfig_{};
sdl3cpp::services::RenderBudgetConfig budgets_{};
sdl3cpp::services::CrashRecoveryConfig crashRecovery_{};
std::string configJson_{};
};

View File

@@ -111,6 +111,8 @@ public:
return materialXMaterials_;
}
const sdl3cpp::services::GuiFontConfig& GetGuiFontConfig() const override { return guiFontConfig_; }
const sdl3cpp::services::RenderBudgetConfig& GetRenderBudgetConfig() const override { return budgets_; }
const sdl3cpp::services::CrashRecoveryConfig& GetCrashRecoveryConfig() const override { return crashRecovery_; }
const std::string& GetConfigJson() const override { return configJson_; }
private:
@@ -125,6 +127,8 @@ private:
sdl3cpp::services::MaterialXConfig materialXConfig_{};
std::vector<sdl3cpp::services::MaterialXMaterialConfig> materialXMaterials_{};
sdl3cpp::services::GuiFontConfig guiFontConfig_{};
sdl3cpp::services::RenderBudgetConfig budgets_{};
sdl3cpp::services::CrashRecoveryConfig crashRecovery_{};
std::string configJson_{};
};
@@ -154,6 +158,8 @@ public:
return materialXMaterials_;
}
const sdl3cpp::services::GuiFontConfig& GetGuiFontConfig() const override { return guiFontConfig_; }
const sdl3cpp::services::RenderBudgetConfig& GetRenderBudgetConfig() const override { return budgets_; }
const sdl3cpp::services::CrashRecoveryConfig& GetCrashRecoveryConfig() const override { return crashRecovery_; }
const std::string& GetConfigJson() const override { return configJson_; }
private:
@@ -170,6 +176,8 @@ private:
sdl3cpp::services::MaterialXConfig materialXConfig_{};
std::vector<sdl3cpp::services::MaterialXMaterialConfig> materialXMaterials_{};
sdl3cpp::services::GuiFontConfig guiFontConfig_{};
sdl3cpp::services::RenderBudgetConfig budgets_{};
sdl3cpp::services::CrashRecoveryConfig crashRecovery_{};
std::string renderer_;
};