ROADMAP.md

This commit is contained in:
2026-01-09 19:44:51 +00:00
parent f3dd71605b
commit b4f60a70c1
29 changed files with 918 additions and 8 deletions

View File

@@ -308,6 +308,7 @@ if(BUILD_SDL3_APP)
src/services/impl/lifecycle_service.cpp
src/services/impl/application_loop_service.cpp
src/services/impl/render_coordinator_service.cpp
src/services/impl/validation_tour_service.cpp
src/services/impl/render_graph_service.cpp
src/services/impl/null_gui_service.cpp
src/services/impl/bgfx_gui_service.cpp

View File

@@ -38,7 +38,7 @@ Treat JSON config as a declarative control plane that compiles into scene, resou
- [~] Explicit pass scheduling and backend submission planning (schedule only; no backend plan)
### Ultra Plan: "Probe Fortress"
- [~] Probe hooks (config/render graph/graphics reports wired; `OnDraw`/`OnPresent`/`OnFrameEnd` now emit trace-gated runtime probes; `OnLoadScene` still missing)
- [~] Probe hooks (config/render graph/graphics reports wired; `OnDraw`/`OnPresent`/`OnFrameEnd`/`OnLoadScene` emit trace-gated runtime probes)
- [x] Pipeline compatibility checks (mesh layout vs shader inputs) via shader pipeline validator
- [x] Sampler limits enforced from bgfx caps
- [ ] Shader uniform compatibility enforcement
@@ -122,7 +122,7 @@ Treat JSON config as a declarative control plane that compiles into scene, resou
- Acceptance: a two-pass graph (offscreen + swapchain) renders correctly.
### Phase 6: Runtime Probe Hooks And Recovery Policy (3-6 days)
- Add runtime probe hooks (`OnDraw`, `OnPresent`, `OnFrameEnd`) in render coordinator + graphics backend.
- Add runtime probe hooks (`OnDraw`, `OnPresent`, `OnFrameEnd`, `OnLoadScene`) in render coordinator + graphics backend/scene service.
- Map probe severity to crash recovery policies.
- Add probes for invalid handles and pass output misuse.
- Deliverable: runtime diagnostics that are structured and actionable.

View File

@@ -255,6 +255,48 @@
},
"additionalProperties": true
},
"validation_tour": {
"type": "object",
"properties": {
"enabled": {"type": "boolean"},
"fail_on_mismatch": {"type": "boolean"},
"warmup_frames": {"type": "integer", "minimum": 0},
"capture_frames": {"type": "integer", "minimum": 0},
"output_dir": {"type": "string"},
"checkpoints": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"camera": {
"type": "object",
"properties": {
"position": {"$ref": "#/definitions/float3"},
"look_at": {"$ref": "#/definitions/float3"},
"up": {"$ref": "#/definitions/float3"},
"fov_degrees": {"type": "number"},
"near": {"type": "number"},
"far": {"type": "number"}
},
"additionalProperties": true
},
"expected": {
"type": "object",
"properties": {
"image": {"type": "string"},
"tolerance": {"type": "number", "minimum": 0, "maximum": 1},
"max_diff_pixels": {"type": "number", "minimum": 0}
},
"additionalProperties": true
}
},
"additionalProperties": true
}
}
},
"additionalProperties": true
},
"gui": {
"type": "object",
"properties": {

View File

@@ -36,10 +36,12 @@
#include "services/impl/crash_recovery_service.hpp"
#include "services/impl/logger_service.hpp"
#include "services/impl/pipeline_compiler_service.hpp"
#include "services/impl/validation_tour_service.hpp"
#include "services/interfaces/i_platform_service.hpp"
#include "services/interfaces/i_probe_service.hpp"
#include "services/interfaces/i_render_graph_service.hpp"
#include "services/interfaces/i_shader_system_registry.hpp"
#include "services/interfaces/i_validation_tour_service.hpp"
#include "services/interfaces/i_config_compiler_service.hpp"
#include <iostream>
#include <stdexcept>
@@ -228,6 +230,12 @@ void ServiceBasedApp::RegisterServices() {
registry_.GetService<services::IProbeService>());
auto configService = registry_.GetService<services::IConfigService>();
// Validation tour service (startup visual checks)
registry_.RegisterService<services::IValidationTourService, services::impl::ValidationTourService>(
configService,
registry_.GetService<services::IProbeService>(),
registry_.GetService<services::ILogger>());
// Render graph service (DAG build + scheduling)
registry_.RegisterService<services::IRenderGraphService, services::impl::RenderGraphService>(
registry_.GetService<services::ILogger>(),
@@ -357,7 +365,8 @@ void ServiceBasedApp::RegisterServices() {
registry_.GetService<services::IShaderScriptService>(),
registry_.GetService<services::IGuiScriptService>(),
registry_.GetService<services::IGuiService>(),
registry_.GetService<services::ISceneService>());
registry_.GetService<services::ISceneService>(),
registry_.GetService<services::IValidationTourService>());
// Application loop service
registry_.RegisterService<services::IApplicationLoopService, services::impl::ApplicationLoopService>(

View File

@@ -1184,6 +1184,19 @@ bool BgfxGraphicsBackend::EndFrame(GraphicsDeviceHandle device) {
return true;
}
void BgfxGraphicsBackend::RequestScreenshot(GraphicsDeviceHandle device,
const std::filesystem::path& outputPath) {
(void)device;
if (!initialized_) {
return;
}
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "RequestScreenshot",
"outputPath=" + outputPath.string());
}
bgfx::requestScreenShot(BGFX_INVALID_HANDLE, outputPath.string().c_str());
}
void BgfxGraphicsBackend::SetViewState(const ViewState& viewState) {
viewState_ = viewState;
bgfx::setViewTransform(viewId_, viewState_.view.data(), viewState_.proj.data());

View File

@@ -9,6 +9,7 @@
#include "../../core/vertex.hpp"
#include <bgfx/bgfx.h>
#include <array>
#include <filesystem>
#include <memory>
#include <unordered_map>
#include <vector>
@@ -49,6 +50,8 @@ public:
GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer,
uint32_t indexOffset, uint32_t indexCount, int32_t vertexOffset,
const std::array<float, 16>& modelMatrix) override;
void RequestScreenshot(GraphicsDeviceHandle device,
const std::filesystem::path& outputPath) override;
GraphicsDeviceHandle GetPhysicalDevice() const override;
std::pair<uint32_t, uint32_t> GetSwapchainExtent() const override;
uint32_t GetSwapchainFormat() const override;

View File

@@ -229,6 +229,17 @@ bool GraphicsService::EndFrame() {
return backend_->EndFrame(device_);
}
void GraphicsService::RequestScreenshot(const std::filesystem::path& outputPath) {
logger_->Trace("GraphicsService", "RequestScreenshot",
"outputPath=" + outputPath.string());
if (!initialized_) {
return;
}
backend_->RequestScreenshot(device_, outputPath);
}
void GraphicsService::WaitIdle() {
logger_->Trace("GraphicsService", "WaitIdle");

View File

@@ -41,6 +41,7 @@ public:
const ViewState& viewState) override;
void ConfigureView(uint16_t viewId, const ViewClearConfig& clearConfig) override;
bool EndFrame() override;
void RequestScreenshot(const std::filesystem::path& outputPath) override;
void WaitIdle() override;
GraphicsDeviceHandle GetDevice() const override;
GraphicsDeviceHandle GetPhysicalDevice() const override;

View File

@@ -461,6 +461,12 @@ bool GxmGraphicsBackend::EndFrame(GraphicsDeviceHandle device) {
return true;
}
void GxmGraphicsBackend::RequestScreenshot(GraphicsDeviceHandle device,
const std::filesystem::path& outputPath) {
(void)device;
(void)outputPath;
}
void GxmGraphicsBackend::SetViewState(const ViewState& viewState) {
std::cout << "GXM: Setting view state" << std::endl;
(void)viewState;

View File

@@ -35,6 +35,8 @@ public:
bool BeginFrame(GraphicsDeviceHandle device) override;
bool EndFrame(GraphicsDeviceHandle device) override;
void RequestScreenshot(GraphicsDeviceHandle device,
const std::filesystem::path& outputPath) override;
void SetViewState(const ViewState& viewState) override;
void ConfigureView(GraphicsDeviceHandle device,

View File

@@ -1190,6 +1190,150 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
}
}
const auto* validationValue = getObjectMember(document, "validation_tour", "validation_tour");
if (validationValue) {
auto readBool = [&](const char* name, bool& target) {
if (!validationValue->HasMember(name)) {
return;
}
const auto& value = (*validationValue)[name];
if (!value.IsBool()) {
throw std::runtime_error("JSON member 'validation_tour." + std::string(name) + "' must be a boolean");
}
target = value.GetBool();
};
readBool("enabled", config.validationTour.enabled);
readBool("fail_on_mismatch", config.validationTour.failOnMismatch);
readUint32(*validationValue, "warmup_frames", "validation_tour", config.validationTour.warmupFrames);
readUint32(*validationValue, "capture_frames", "validation_tour", config.validationTour.captureFrames);
if (validationValue->HasMember("output_dir")) {
const auto& value = (*validationValue)["output_dir"];
if (!value.IsString()) {
throw std::runtime_error("JSON member 'validation_tour.output_dir' must be a string");
}
config.validationTour.outputDir = value.GetString();
}
if (validationValue->HasMember("checkpoints")) {
const auto& checkpointsValue = (*validationValue)["checkpoints"];
if (!checkpointsValue.IsArray()) {
throw std::runtime_error("JSON member 'validation_tour.checkpoints' must be an array");
}
config.validationTour.checkpoints.clear();
config.validationTour.checkpoints.reserve(checkpointsValue.Size());
auto readFloat3 = [&](const rapidjson::Value& value,
const std::string& path,
std::array<float, 3>& target) {
if (!value.IsArray() || value.Size() != 3) {
throw std::runtime_error("JSON member '" + path + "' must be an array of 3 numbers");
}
for (rapidjson::SizeType i = 0; i < 3; ++i) {
if (!value[i].IsNumber()) {
throw std::runtime_error("JSON member '" + path + "[" + std::to_string(i) +
"]' must be a number");
}
target[i] = static_cast<float>(value[i].GetDouble());
}
};
for (rapidjson::SizeType i = 0; i < checkpointsValue.Size(); ++i) {
const auto& entry = checkpointsValue[i];
const std::string basePath = "validation_tour.checkpoints[" + std::to_string(i) + "]";
if (!entry.IsObject()) {
throw std::runtime_error("JSON member '" + basePath + "' must be an object");
}
ValidationCheckpointConfig checkpoint;
if (!entry.HasMember("id") || !entry["id"].IsString()) {
throw std::runtime_error("JSON member '" + basePath + ".id' must be a string");
}
checkpoint.id = entry["id"].GetString();
if (!entry.HasMember("camera") || !entry["camera"].IsObject()) {
throw std::runtime_error("JSON member '" + basePath + ".camera' must be an object");
}
const auto& cameraValue = entry["camera"];
if (!cameraValue.HasMember("position")) {
throw std::runtime_error("JSON member '" + basePath + ".camera.position' is required");
}
readFloat3(cameraValue["position"], basePath + ".camera.position",
checkpoint.camera.position);
if (!cameraValue.HasMember("look_at")) {
throw std::runtime_error("JSON member '" + basePath + ".camera.look_at' is required");
}
readFloat3(cameraValue["look_at"], basePath + ".camera.look_at",
checkpoint.camera.lookAt);
if (cameraValue.HasMember("up")) {
readFloat3(cameraValue["up"], basePath + ".camera.up",
checkpoint.camera.up);
}
if (cameraValue.HasMember("fov_degrees")) {
const auto& value = cameraValue["fov_degrees"];
if (!value.IsNumber()) {
throw std::runtime_error("JSON member '" + basePath +
".camera.fov_degrees' must be a number");
}
checkpoint.camera.fovDegrees = static_cast<float>(value.GetDouble());
}
if (cameraValue.HasMember("near")) {
const auto& value = cameraValue["near"];
if (!value.IsNumber()) {
throw std::runtime_error("JSON member '" + basePath +
".camera.near' must be a number");
}
checkpoint.camera.nearPlane = static_cast<float>(value.GetDouble());
}
if (cameraValue.HasMember("far")) {
const auto& value = cameraValue["far"];
if (!value.IsNumber()) {
throw std::runtime_error("JSON member '" + basePath +
".camera.far' must be a number");
}
checkpoint.camera.farPlane = static_cast<float>(value.GetDouble());
}
if (!entry.HasMember("expected") || !entry["expected"].IsObject()) {
throw std::runtime_error("JSON member '" + basePath + ".expected' must be an object");
}
const auto& expectedValue = entry["expected"];
if (!expectedValue.HasMember("image") || !expectedValue["image"].IsString()) {
throw std::runtime_error("JSON member '" + basePath + ".expected.image' must be a string");
}
checkpoint.expected.imagePath = expectedValue["image"].GetString();
if (expectedValue.HasMember("tolerance")) {
const auto& value = expectedValue["tolerance"];
if (!value.IsNumber()) {
throw std::runtime_error("JSON member '" + basePath +
".expected.tolerance' must be a number");
}
const double rawValue = value.GetDouble();
if (rawValue < 0.0 || rawValue > 1.0) {
throw std::runtime_error("JSON member '" + basePath +
".expected.tolerance' must be between 0 and 1");
}
checkpoint.expected.tolerance = static_cast<float>(rawValue);
}
if (expectedValue.HasMember("max_diff_pixels")) {
const auto& value = expectedValue["max_diff_pixels"];
if (!value.IsNumber()) {
throw std::runtime_error("JSON member '" + basePath +
".expected.max_diff_pixels' must be a number");
}
const double rawValue = value.GetDouble();
if (rawValue < 0.0) {
throw std::runtime_error("JSON member '" + basePath +
".expected.max_diff_pixels' must be non-negative");
}
checkpoint.expected.maxDiffPixels = static_cast<size_t>(rawValue);
}
config.validationTour.checkpoints.push_back(std::move(checkpoint));
}
}
}
return config;
}
@@ -1353,6 +1497,59 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config,
static_cast<uint64_t>(config.crashRecovery.maxMemoryWarnings), allocator);
document.AddMember("crash_recovery", crashObject, allocator);
rapidjson::Value validationObject(rapidjson::kObjectType);
validationObject.AddMember("enabled", config.validationTour.enabled, allocator);
validationObject.AddMember("fail_on_mismatch", config.validationTour.failOnMismatch, allocator);
validationObject.AddMember("warmup_frames", config.validationTour.warmupFrames, allocator);
validationObject.AddMember("capture_frames", config.validationTour.captureFrames, allocator);
addStringMember(validationObject, "output_dir", config.validationTour.outputDir.string());
if (!config.validationTour.checkpoints.empty()) {
rapidjson::Value checkpointsArray(rapidjson::kArrayType);
for (const auto& checkpoint : config.validationTour.checkpoints) {
rapidjson::Value entry(rapidjson::kObjectType);
entry.AddMember("id", rapidjson::Value(checkpoint.id.c_str(), allocator), allocator);
rapidjson::Value cameraObject(rapidjson::kObjectType);
rapidjson::Value cameraPosition(rapidjson::kArrayType);
cameraPosition.PushBack(checkpoint.camera.position[0], allocator);
cameraPosition.PushBack(checkpoint.camera.position[1], allocator);
cameraPosition.PushBack(checkpoint.camera.position[2], allocator);
cameraObject.AddMember("position", cameraPosition, allocator);
rapidjson::Value cameraLookAt(rapidjson::kArrayType);
cameraLookAt.PushBack(checkpoint.camera.lookAt[0], allocator);
cameraLookAt.PushBack(checkpoint.camera.lookAt[1], allocator);
cameraLookAt.PushBack(checkpoint.camera.lookAt[2], allocator);
cameraObject.AddMember("look_at", cameraLookAt, allocator);
rapidjson::Value cameraUp(rapidjson::kArrayType);
cameraUp.PushBack(checkpoint.camera.up[0], allocator);
cameraUp.PushBack(checkpoint.camera.up[1], allocator);
cameraUp.PushBack(checkpoint.camera.up[2], allocator);
cameraObject.AddMember("up", cameraUp, allocator);
cameraObject.AddMember("fov_degrees", checkpoint.camera.fovDegrees, allocator);
cameraObject.AddMember("near", checkpoint.camera.nearPlane, allocator);
cameraObject.AddMember("far", checkpoint.camera.farPlane, allocator);
entry.AddMember("camera", cameraObject, allocator);
rapidjson::Value expectedObject(rapidjson::kObjectType);
expectedObject.AddMember("image",
rapidjson::Value(checkpoint.expected.imagePath.string().c_str(), allocator),
allocator);
expectedObject.AddMember("tolerance", checkpoint.expected.tolerance, allocator);
expectedObject.AddMember("max_diff_pixels",
static_cast<uint64_t>(checkpoint.expected.maxDiffPixels),
allocator);
entry.AddMember("expected", expectedObject, allocator);
checkpointsArray.PushBack(entry, allocator);
}
validationObject.AddMember("checkpoints", checkpointsArray, allocator);
}
document.AddMember("validation_tour", validationObject, allocator);
rapidjson::Value bindingsObject(rapidjson::kObjectType);
auto addBindingMember = [&](const char* name, const std::string& value) {
rapidjson::Value nameValue(name, allocator);

View File

@@ -140,6 +140,12 @@ public:
}
return config_.crashRecovery;
}
const ValidationTourConfig& GetValidationTourConfig() const override {
if (logger_) {
logger_->Trace("JsonConfigService", "GetValidationTourConfig");
}
return config_.validationTour;
}
const std::string& GetConfigJson() const override {
if (logger_) {
logger_->Trace("JsonConfigService", "GetConfigJson");

View File

@@ -252,6 +252,59 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std
static_cast<uint64_t>(config.crashRecovery.maxMemoryWarnings), allocator);
document.AddMember("crash_recovery", crashObject, allocator);
rapidjson::Value validationObject(rapidjson::kObjectType);
validationObject.AddMember("enabled", config.validationTour.enabled, allocator);
validationObject.AddMember("fail_on_mismatch", config.validationTour.failOnMismatch, allocator);
validationObject.AddMember("warmup_frames", config.validationTour.warmupFrames, allocator);
validationObject.AddMember("capture_frames", config.validationTour.captureFrames, allocator);
addStringMember(validationObject, "output_dir", config.validationTour.outputDir.string());
if (!config.validationTour.checkpoints.empty()) {
rapidjson::Value checkpointsArray(rapidjson::kArrayType);
for (const auto& checkpoint : config.validationTour.checkpoints) {
rapidjson::Value entry(rapidjson::kObjectType);
entry.AddMember("id", rapidjson::Value(checkpoint.id.c_str(), allocator), allocator);
rapidjson::Value cameraObject(rapidjson::kObjectType);
rapidjson::Value cameraPosition(rapidjson::kArrayType);
cameraPosition.PushBack(checkpoint.camera.position[0], allocator);
cameraPosition.PushBack(checkpoint.camera.position[1], allocator);
cameraPosition.PushBack(checkpoint.camera.position[2], allocator);
cameraObject.AddMember("position", cameraPosition, allocator);
rapidjson::Value cameraLookAt(rapidjson::kArrayType);
cameraLookAt.PushBack(checkpoint.camera.lookAt[0], allocator);
cameraLookAt.PushBack(checkpoint.camera.lookAt[1], allocator);
cameraLookAt.PushBack(checkpoint.camera.lookAt[2], allocator);
cameraObject.AddMember("look_at", cameraLookAt, allocator);
rapidjson::Value cameraUp(rapidjson::kArrayType);
cameraUp.PushBack(checkpoint.camera.up[0], allocator);
cameraUp.PushBack(checkpoint.camera.up[1], allocator);
cameraUp.PushBack(checkpoint.camera.up[2], allocator);
cameraObject.AddMember("up", cameraUp, allocator);
cameraObject.AddMember("fov_degrees", checkpoint.camera.fovDegrees, allocator);
cameraObject.AddMember("near", checkpoint.camera.nearPlane, allocator);
cameraObject.AddMember("far", checkpoint.camera.farPlane, allocator);
entry.AddMember("camera", cameraObject, allocator);
rapidjson::Value expectedObject(rapidjson::kObjectType);
expectedObject.AddMember("image",
rapidjson::Value(checkpoint.expected.imagePath.string().c_str(), allocator),
allocator);
expectedObject.AddMember("tolerance", checkpoint.expected.tolerance, allocator);
expectedObject.AddMember("max_diff_pixels",
static_cast<uint64_t>(checkpoint.expected.maxDiffPixels),
allocator);
entry.AddMember("expected", expectedObject, allocator);
checkpointsArray.PushBack(entry, allocator);
}
validationObject.AddMember("checkpoints", checkpointsArray, allocator);
}
document.AddMember("validation_tour", validationObject, allocator);
rapidjson::Value guiObject(rapidjson::kObjectType);
rapidjson::Value fontObject(rapidjson::kObjectType);
fontObject.AddMember("use_freetype", config.guiFont.useFreeType, allocator);

View File

@@ -1,6 +1,7 @@
#include "render_coordinator_service.hpp"
#include <algorithm>
#include <stdexcept>
#include <unordered_map>
#include <unordered_set>
#include <vector>
@@ -15,7 +16,8 @@ RenderCoordinatorService::RenderCoordinatorService(std::shared_ptr<ILogger> logg
std::shared_ptr<IShaderScriptService> shaderScriptService,
std::shared_ptr<IGuiScriptService> guiScriptService,
std::shared_ptr<IGuiService> guiService,
std::shared_ptr<ISceneService> sceneService)
std::shared_ptr<ISceneService> sceneService,
std::shared_ptr<IValidationTourService> validationTourService)
: logger_(std::move(logger)),
configService_(std::move(configService)),
configCompilerService_(std::move(configCompilerService)),
@@ -24,7 +26,8 @@ RenderCoordinatorService::RenderCoordinatorService(std::shared_ptr<ILogger> logg
shaderScriptService_(std::move(shaderScriptService)),
guiScriptService_(std::move(guiScriptService)),
guiService_(std::move(guiService)),
sceneService_(std::move(sceneService)) {
sceneService_(std::move(sceneService)),
validationTourService_(std::move(validationTourService)) {
if (logger_) {
logger_->Trace("RenderCoordinatorService", "RenderCoordinatorService",
"configService=" + std::string(configService_ ? "set" : "null") +
@@ -34,7 +37,8 @@ RenderCoordinatorService::RenderCoordinatorService(std::shared_ptr<ILogger> logg
", shaderScriptService=" + std::string(shaderScriptService_ ? "set" : "null") +
", guiScriptService=" + std::string(guiScriptService_ ? "set" : "null") +
", guiService=" + std::string(guiService_ ? "set" : "null") +
", sceneService=" + std::string(sceneService_ ? "set" : "null"),
", sceneService=" + std::string(sceneService_ ? "set" : "null") +
", validationTourService=" + std::string(validationTourService_ ? "set" : "null"),
"Created");
}
}
@@ -132,6 +136,8 @@ void RenderCoordinatorService::RenderFrame(float time) {
logger_->Trace("RenderCoordinatorService", "RenderFrame", "time=" + std::to_string(time), "Entering");
}
ValidationFramePlan validationPlan{};
if (!graphicsService_) {
if (logger_) {
logger_->Error("RenderCoordinatorService::RenderFrame: Graphics service not available");
@@ -234,11 +240,22 @@ void RenderCoordinatorService::RenderFrame(float time) {
auto extent = graphicsService_->GetSwapchainExtent();
float aspect = extent.second == 0 ? 1.0f
: static_cast<float>(extent.first) / static_cast<float>(extent.second);
auto viewState = sceneScriptService_->GetViewState(aspect);
if (validationTourService_) {
validationPlan = validationTourService_->BeginFrame(aspect);
}
ViewState viewState = sceneScriptService_->GetViewState(aspect);
if (validationPlan.active && validationPlan.overrideView) {
viewState = validationPlan.viewState;
}
graphicsService_->RenderScene(renderCommands, viewState);
}
if (validationPlan.active && validationPlan.requestScreenshot) {
graphicsService_->RequestScreenshot(validationPlan.screenshotPath);
}
if (!graphicsService_->EndFrame()) {
if (logger_) {
logger_->Warn("RenderCoordinatorService::RenderFrame: Swapchain out of date during EndFrame");
@@ -247,6 +264,16 @@ void RenderCoordinatorService::RenderFrame(float time) {
return;
}
if (validationPlan.active && validationTourService_) {
ValidationFrameResult validationResult = validationTourService_->EndFrame();
if (validationResult.shouldAbort) {
if (logger_) {
logger_->Error("RenderCoordinatorService::RenderFrame: Validation failed - " + validationResult.message);
}
throw std::runtime_error("Validation tour failed: " + validationResult.message);
}
}
if (logger_) {
logger_->Trace("RenderCoordinatorService", "RenderFrame", "", "Exiting");
}

View File

@@ -10,6 +10,7 @@
#include "../interfaces/i_scene_script_service.hpp"
#include "../interfaces/i_scene_service.hpp"
#include "../interfaces/i_shader_script_service.hpp"
#include "../interfaces/i_validation_tour_service.hpp"
#include <memory>
namespace sdl3cpp::services::impl {
@@ -24,7 +25,8 @@ public:
std::shared_ptr<IShaderScriptService> shaderScriptService,
std::shared_ptr<IGuiScriptService> guiScriptService,
std::shared_ptr<IGuiService> guiService,
std::shared_ptr<ISceneService> sceneService);
std::shared_ptr<ISceneService> sceneService,
std::shared_ptr<IValidationTourService> validationTourService);
~RenderCoordinatorService() override = default;
void RenderFrame(float time) override;
@@ -41,6 +43,7 @@ private:
std::shared_ptr<IGuiScriptService> guiScriptService_;
std::shared_ptr<IGuiService> guiService_;
std::shared_ptr<ISceneService> sceneService_;
std::shared_ptr<IValidationTourService> validationTourService_;
size_t lastVertexCount_ = 0;
size_t lastIndexCount_ = 0;
bool shadersLoaded_ = false;

View File

@@ -0,0 +1,329 @@
#include "validation_tour_service.hpp"
#include "../interfaces/probe_types.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <stb_image.h>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <filesystem>
namespace sdl3cpp::services::impl {
namespace {
std::array<float, 16> ToArray(const glm::mat4& matrix) {
std::array<float, 16> result{};
std::memcpy(result.data(), glm::value_ptr(matrix), sizeof(float) * result.size());
return result;
}
}
ValidationTourService::ValidationTourService(std::shared_ptr<IConfigService> configService,
std::shared_ptr<IProbeService> probeService,
std::shared_ptr<ILogger> logger)
: probeService_(std::move(probeService)),
logger_(std::move(logger)) {
if (configService) {
config_ = configService->GetValidationTourConfig();
}
if (config_.enabled && config_.checkpoints.empty()) {
if (logger_) {
logger_->Warn("ValidationTourService: validation_tour enabled with no checkpoints");
}
active_ = false;
completed_ = true;
return;
}
if (config_.enabled && config_.captureFrames == 0) {
if (logger_) {
logger_->Warn("ValidationTourService: capture_frames was 0; defaulting to 1");
}
config_.captureFrames = 1;
}
active_ = config_.enabled;
if (!active_) {
completed_ = true;
return;
}
resolvedOutputDir_ = ResolvePath(config_.outputDir);
if (!resolvedOutputDir_.empty()) {
std::error_code ec;
std::filesystem::create_directories(resolvedOutputDir_, ec);
if (ec && logger_) {
logger_->Warn("ValidationTourService: Failed to create output dir " +
resolvedOutputDir_.string() + " error=" + ec.message());
}
}
checkpointIndex_ = 0;
AdvanceCheckpoint();
}
ValidationFramePlan ValidationTourService::BeginFrame(float aspect) {
ValidationFramePlan plan{};
if (!IsActive()) {
return plan;
}
if (!pendingCapture_ && warmupRemaining_ == 0 && capturesRemaining_ == 0) {
++checkpointIndex_;
AdvanceCheckpoint();
if (!IsActive()) {
return plan;
}
}
const auto& checkpoint = config_.checkpoints[checkpointIndex_];
plan.active = true;
plan.overrideView = true;
plan.viewState = BuildViewState(checkpoint.camera, aspect);
if (!pendingCapture_ && warmupRemaining_ == 0 && capturesRemaining_ > 0) {
std::filesystem::path outputDir = resolvedOutputDir_.empty()
? ResolvePath(config_.outputDir)
: resolvedOutputDir_;
std::string captureName = checkpoint.id + "_capture_" + std::to_string(captureIndex_) + ".png";
std::filesystem::path actualPath = outputDir.empty()
? std::filesystem::path(captureName)
: outputDir / captureName;
plan.requestScreenshot = true;
plan.screenshotPath = actualPath;
PendingCapture pending{};
pending.actualPath = actualPath;
pending.expectedPath = ResolvePath(checkpoint.expected.imagePath);
pending.checkpointId = checkpoint.id;
pending.tolerance = checkpoint.expected.tolerance;
pending.maxDiffPixels = checkpoint.expected.maxDiffPixels;
pending.captureIndex = captureIndex_;
pendingCapture_ = std::move(pending);
if (logger_) {
logger_->Trace("ValidationTourService", "BeginFrame",
"checkpoint=" + checkpoint.id +
", captureIndex=" + std::to_string(captureIndex_) +
", output=" + actualPath.string());
}
} else if (logger_) {
logger_->Trace("ValidationTourService", "BeginFrame",
"checkpoint=" + checkpoint.id +
", warmupRemaining=" + std::to_string(warmupRemaining_) +
", capturesRemaining=" + std::to_string(capturesRemaining_) +
", pendingCapture=" + std::string(pendingCapture_.has_value() ? "true" : "false"));
}
return plan;
}
ValidationFrameResult ValidationTourService::EndFrame() {
ValidationFrameResult result{};
if (!IsActive()) {
if (failed_) {
result.shouldAbort = config_.failOnMismatch;
result.message = failureMessage_;
}
return result;
}
if (pendingCapture_) {
const auto& pending = *pendingCapture_;
if (!std::filesystem::exists(pending.actualPath)) {
return result;
}
std::string errorMessage;
bool ok = CompareImages(pending, errorMessage);
if (!ok) {
failed_ = true;
failureMessage_ = errorMessage;
result.shouldAbort = config_.failOnMismatch;
result.message = failureMessage_;
pendingCapture_.reset();
return result;
}
pendingCapture_.reset();
if (capturesRemaining_ > 0) {
--capturesRemaining_;
}
++captureIndex_;
if (capturesRemaining_ == 0) {
++checkpointIndex_;
AdvanceCheckpoint();
}
return result;
}
if (warmupRemaining_ > 0) {
--warmupRemaining_;
}
return result;
}
void ValidationTourService::AdvanceCheckpoint() {
if (checkpointIndex_ >= config_.checkpoints.size()) {
completed_ = true;
active_ = false;
if (logger_) {
logger_->Trace("ValidationTourService", "AdvanceCheckpoint", "", "Validation tour completed");
}
return;
}
warmupRemaining_ = config_.warmupFrames;
capturesRemaining_ = config_.captureFrames;
captureIndex_ = 0;
if (logger_) {
logger_->Trace("ValidationTourService", "AdvanceCheckpoint",
"checkpoint=" + config_.checkpoints[checkpointIndex_].id +
", warmupFrames=" + std::to_string(warmupRemaining_) +
", captureFrames=" + std::to_string(capturesRemaining_));
}
}
ViewState ValidationTourService::BuildViewState(const ValidationCameraConfig& camera, float aspect) const {
glm::vec3 position(camera.position[0], camera.position[1], camera.position[2]);
glm::vec3 lookAt(camera.lookAt[0], camera.lookAt[1], camera.lookAt[2]);
glm::vec3 up(camera.up[0], camera.up[1], camera.up[2]);
glm::mat4 view = glm::lookAt(position, lookAt, up);
float clampedAspect = aspect <= 0.0f ? 1.0f : aspect;
glm::mat4 proj = glm::perspective(glm::radians(camera.fovDegrees), clampedAspect,
camera.nearPlane, camera.farPlane);
ViewState state{};
state.view = ToArray(view);
state.proj = ToArray(proj);
glm::mat4 viewProj = proj * view;
state.viewProj = ToArray(viewProj);
state.cameraPosition = camera.position;
return state;
}
std::filesystem::path ValidationTourService::ResolvePath(const std::filesystem::path& path) const {
if (path.empty()) {
return {};
}
if (path.is_absolute()) {
return path;
}
return std::filesystem::current_path() / path;
}
bool ValidationTourService::CompareImages(const PendingCapture& pending, std::string& errorMessage) const {
int actualWidth = 0;
int actualHeight = 0;
int actualChannels = 0;
stbi_uc* actualPixels = stbi_load(pending.actualPath.string().c_str(),
&actualWidth,
&actualHeight,
&actualChannels,
STBI_rgb_alpha);
if (!actualPixels) {
errorMessage = "Validation capture missing or unreadable: " + pending.actualPath.string();
ReportMismatch(pending, "Capture missing", errorMessage);
return false;
}
int expectedWidth = 0;
int expectedHeight = 0;
int expectedChannels = 0;
stbi_uc* expectedPixels = stbi_load(pending.expectedPath.string().c_str(),
&expectedWidth,
&expectedHeight,
&expectedChannels,
STBI_rgb_alpha);
if (!expectedPixels) {
stbi_image_free(actualPixels);
errorMessage = "Expected image missing or unreadable: " + pending.expectedPath.string();
ReportMismatch(pending, "Expected missing", errorMessage);
return false;
}
if (actualWidth != expectedWidth || actualHeight != expectedHeight) {
stbi_image_free(actualPixels);
stbi_image_free(expectedPixels);
errorMessage = "Validation image size mismatch for checkpoint '" + pending.checkpointId +
"' actual=" + std::to_string(actualWidth) + "x" + std::to_string(actualHeight) +
" expected=" + std::to_string(expectedWidth) + "x" + std::to_string(expectedHeight);
ReportMismatch(pending, "Image size mismatch", errorMessage);
return false;
}
const size_t pixelCount = static_cast<size_t>(actualWidth) * static_cast<size_t>(actualHeight);
const size_t totalChannels = pixelCount * 4;
size_t mismatchCount = 0;
float maxChannelDiff = 0.0f;
double totalDiff = 0.0;
for (size_t idx = 0; idx < totalChannels; idx += 4) {
bool pixelMismatch = false;
for (size_t channel = 0; channel < 4; ++channel) {
int actual = actualPixels[idx + channel];
int expected = expectedPixels[idx + channel];
float diff = static_cast<float>(std::abs(actual - expected)) / 255.0f;
totalDiff += static_cast<double>(diff);
if (diff > maxChannelDiff) {
maxChannelDiff = diff;
}
if (diff > pending.tolerance) {
pixelMismatch = true;
}
}
if (pixelMismatch) {
++mismatchCount;
}
}
stbi_image_free(actualPixels);
stbi_image_free(expectedPixels);
const double averageDiff = totalChannels == 0 ? 0.0 : totalDiff / static_cast<double>(totalChannels);
if (mismatchCount > pending.maxDiffPixels) {
errorMessage = "Validation mismatch for checkpoint '" + pending.checkpointId +
"' mismatchedPixels=" + std::to_string(mismatchCount) +
" maxAllowed=" + std::to_string(pending.maxDiffPixels) +
" tolerance=" + std::to_string(pending.tolerance) +
" avgDiff=" + std::to_string(averageDiff) +
" maxChannelDiff=" + std::to_string(maxChannelDiff) +
" actual=" + pending.actualPath.string() +
" expected=" + pending.expectedPath.string();
ReportMismatch(pending, "Image mismatch", errorMessage);
return false;
}
if (logger_) {
logger_->Trace("ValidationTourService", "CompareImages",
"checkpoint=" + pending.checkpointId +
", mismatchedPixels=" + std::to_string(mismatchCount) +
", avgDiff=" + std::to_string(averageDiff));
}
return true;
}
void ValidationTourService::ReportMismatch(const PendingCapture& pending,
const std::string& summary,
const std::string& details) const {
if (logger_) {
logger_->Error("ValidationTourService::CompareImages: " + details);
}
if (!probeService_) {
return;
}
ProbeReport report{};
report.severity = ProbeSeverity::Error;
report.code = "VALIDATION_MISMATCH";
report.resourceId = pending.checkpointId;
report.message = summary;
report.details = details;
probeService_->Report(report);
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,55 @@
#pragma once
#include "../interfaces/i_config_service.hpp"
#include "../interfaces/i_logger.hpp"
#include "../interfaces/i_probe_service.hpp"
#include "../interfaces/i_validation_tour_service.hpp"
#include <filesystem>
#include <optional>
namespace sdl3cpp::services::impl {
class ValidationTourService final : public IValidationTourService {
public:
ValidationTourService(std::shared_ptr<IConfigService> configService,
std::shared_ptr<IProbeService> probeService,
std::shared_ptr<ILogger> logger);
ValidationFramePlan BeginFrame(float aspect) override;
ValidationFrameResult EndFrame() override;
bool IsActive() const override { return active_ && !completed_ && !failed_; }
private:
struct PendingCapture {
std::filesystem::path actualPath;
std::filesystem::path expectedPath;
std::string checkpointId;
float tolerance = 0.01f;
size_t maxDiffPixels = 0;
size_t captureIndex = 0;
};
void AdvanceCheckpoint();
ViewState BuildViewState(const ValidationCameraConfig& camera, float aspect) const;
std::filesystem::path ResolvePath(const std::filesystem::path& path) const;
bool CompareImages(const PendingCapture& pending, std::string& errorMessage) const;
void ReportMismatch(const PendingCapture& pending,
const std::string& summary,
const std::string& details) const;
std::shared_ptr<IProbeService> probeService_;
std::shared_ptr<ILogger> logger_;
ValidationTourConfig config_{};
std::filesystem::path resolvedOutputDir_{};
size_t checkpointIndex_ = 0;
uint32_t warmupRemaining_ = 0;
uint32_t capturesRemaining_ = 0;
size_t captureIndex_ = 0;
bool active_ = false;
bool completed_ = false;
bool failed_ = false;
std::string failureMessage_;
std::optional<PendingCapture> pendingCapture_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -149,6 +149,48 @@ struct CrashRecoveryConfig {
size_t maxMemoryWarnings = 3;
};
/**
* @brief Camera configuration for validation checkpoints.
*/
struct ValidationCameraConfig {
std::array<float, 3> position = {0.0f, 0.0f, 0.0f};
std::array<float, 3> lookAt = {0.0f, 0.0f, -1.0f};
std::array<float, 3> up = {0.0f, 1.0f, 0.0f};
float fovDegrees = 60.0f;
float nearPlane = 0.1f;
float farPlane = 1000.0f;
};
/**
* @brief Expected output for a validation checkpoint.
*/
struct ValidationExpectedConfig {
std::filesystem::path imagePath;
float tolerance = 0.01f;
size_t maxDiffPixels = 0;
};
/**
* @brief A single validation checkpoint definition.
*/
struct ValidationCheckpointConfig {
std::string id;
ValidationCameraConfig camera{};
ValidationExpectedConfig expected{};
};
/**
* @brief Config-driven validation tour at startup.
*/
struct ValidationTourConfig {
bool enabled = false;
bool failOnMismatch = true;
uint32_t warmupFrames = 2;
uint32_t captureFrames = 1;
std::filesystem::path outputDir = "artifacts/validation";
std::vector<ValidationCheckpointConfig> checkpoints{};
};
/**
* @brief Runtime configuration values used across services.
*/
@@ -169,6 +211,7 @@ struct RuntimeConfig {
GuiFontConfig guiFont{};
float guiOpacity = 1.0f;
CrashRecoveryConfig crashRecovery{};
ValidationTourConfig validationTour{};
};
} // namespace sdl3cpp::services

View File

@@ -102,6 +102,12 @@ public:
*/
virtual const CrashRecoveryConfig& GetCrashRecoveryConfig() const = 0;
/**
* @brief Get validation tour configuration.
* @return Validation tour configuration
*/
virtual const ValidationTourConfig& GetValidationTourConfig() const = 0;
/**
* @brief Get the full JSON configuration as a string.
*

View File

@@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
#include <array>
@@ -139,6 +140,15 @@ public:
*/
virtual bool EndFrame(GraphicsDeviceHandle device) = 0;
/**
* @brief Request a screenshot of the backbuffer.
*
* @param device Device handle
* @param outputPath Output path for the screenshot
*/
virtual void RequestScreenshot(GraphicsDeviceHandle device,
const std::filesystem::path& outputPath) = 0;
/**
* @brief Set the view state for the current frame.
*

View File

@@ -5,6 +5,7 @@
#include "i_graphics_backend.hpp"
#include <array>
#include <cstdint>
#include <filesystem>
#include <string>
#include <unordered_map>
#include <vector>
@@ -108,6 +109,13 @@ public:
*/
virtual bool EndFrame() = 0;
/**
* @brief Request a screenshot of the backbuffer.
*
* @param outputPath Output path for the screenshot
*/
virtual void RequestScreenshot(const std::filesystem::path& outputPath) = 0;
/**
* @brief Wait for all GPU operations to complete.
*

View File

@@ -0,0 +1,50 @@
#pragma once
#include "graphics_types.hpp"
#include <filesystem>
#include <string>
namespace sdl3cpp::services {
struct ValidationFramePlan {
bool active = false;
bool overrideView = false;
bool requestScreenshot = false;
ViewState viewState{};
std::filesystem::path screenshotPath;
};
struct ValidationFrameResult {
bool shouldAbort = false;
std::string message;
};
/**
* @brief Validation tour interface for startup visual checks.
*/
class IValidationTourService {
public:
virtual ~IValidationTourService() = default;
/**
* @brief Prepare validation state for the upcoming frame.
*
* @param aspect Aspect ratio for view/projection math
* @return Frame plan including optional view override and screenshot request
*/
virtual ValidationFramePlan BeginFrame(float aspect) = 0;
/**
* @brief Finalize validation work after a frame completes.
*
* @return Result indicating whether to abort runtime
*/
virtual ValidationFrameResult EndFrame() = 0;
/**
* @brief Whether validation tour is enabled and active.
*/
virtual bool IsActive() const = 0;
};
} // namespace sdl3cpp::services

View File

@@ -97,6 +97,9 @@ public:
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 sdl3cpp::services::ValidationTourConfig& GetValidationTourConfig() const override {
return validationTour_;
}
const std::string& GetConfigJson() const override { return configJson_; }
private:
@@ -108,6 +111,7 @@ private:
sdl3cpp::services::GuiFontConfig guiFontConfig_{};
sdl3cpp::services::RenderBudgetConfig budgets_{};
sdl3cpp::services::CrashRecoveryConfig crashRecovery_{};
sdl3cpp::services::ValidationTourConfig validationTour_{};
std::string configJson_{};
};

View File

@@ -80,6 +80,9 @@ public:
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 sdl3cpp::services::ValidationTourConfig& GetValidationTourConfig() const override {
return validationTour_;
}
const std::string& GetConfigJson() const override { return configJson_; }
private:
@@ -91,6 +94,7 @@ private:
sdl3cpp::services::GuiFontConfig guiFontConfig_{};
sdl3cpp::services::RenderBudgetConfig budgets_{};
sdl3cpp::services::CrashRecoveryConfig crashRecovery_{};
sdl3cpp::services::ValidationTourConfig validationTour_{};
std::string configJson_{};
};

View File

@@ -105,6 +105,8 @@ public:
bool BeginFrame(sdl3cpp::services::GraphicsDeviceHandle) override { return true; }
bool EndFrame(sdl3cpp::services::GraphicsDeviceHandle) override { return true; }
void RequestScreenshot(sdl3cpp::services::GraphicsDeviceHandle,
const std::filesystem::path&) override {}
void SetViewState(const sdl3cpp::services::ViewState&) override {}
void ConfigureView(sdl3cpp::services::GraphicsDeviceHandle,
uint16_t,

View File

@@ -49,6 +49,9 @@ public:
calls.push_back("EndFrame");
return endFrameResult;
}
void RequestScreenshot(const std::filesystem::path&) override {
calls.push_back("RequestScreenshot");
}
void WaitIdle() override {}
sdl3cpp::services::GraphicsDeviceHandle GetDevice() const override { return nullptr; }
sdl3cpp::services::GraphicsDeviceHandle GetPhysicalDevice() const override { return nullptr; }
@@ -88,6 +91,9 @@ public:
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 sdl3cpp::services::ValidationTourConfig& GetValidationTourConfig() const override {
return validationTour_;
}
const std::string& GetConfigJson() const override { return configJson_; }
private:
@@ -100,6 +106,7 @@ private:
sdl3cpp::services::GuiFontConfig guiFontConfig_{};
sdl3cpp::services::RenderBudgetConfig budgets_{};
sdl3cpp::services::CrashRecoveryConfig crashRecovery_{};
sdl3cpp::services::ValidationTourConfig validationTour_{};
std::string configJson_{};
};
@@ -157,6 +164,7 @@ TEST(RenderCoordinatorInitOrderTest, LoadsShadersOnlyAfterFirstFrame) {
shaderScriptService,
nullptr,
nullptr,
nullptr,
nullptr);
service.RenderFrame(0.0f);
@@ -223,6 +231,7 @@ TEST(RenderCoordinatorRenderGraphTest, ConfiguresViewsInPassOrder) {
nullptr,
nullptr,
nullptr,
nullptr,
nullptr);
service.RenderFrame(0.0f);

View File

@@ -30,6 +30,9 @@ public:
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 sdl3cpp::services::ValidationTourConfig& GetValidationTourConfig() const override {
return validationTour_;
}
const std::string& GetConfigJson() const override { return configJson_; }
private:
@@ -41,6 +44,7 @@ private:
sdl3cpp::services::GuiFontConfig guiFontConfig_{};
sdl3cpp::services::RenderBudgetConfig budgets_{};
sdl3cpp::services::CrashRecoveryConfig crashRecovery_{};
sdl3cpp::services::ValidationTourConfig validationTour_{};
std::string configJson_;
};

View File

@@ -153,6 +153,9 @@ public:
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 sdl3cpp::services::ValidationTourConfig& GetValidationTourConfig() const override {
return validationTour_;
}
const std::string& GetConfigJson() const override { return configJson_; }
void DisableFreeType() {
@@ -180,6 +183,7 @@ private:
sdl3cpp::services::GuiFontConfig guiFontConfig_{};
sdl3cpp::services::RenderBudgetConfig budgets_{};
sdl3cpp::services::CrashRecoveryConfig crashRecovery_{};
sdl3cpp::services::ValidationTourConfig validationTour_{};
std::string configJson_{};
};

View File

@@ -117,6 +117,9 @@ public:
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 sdl3cpp::services::ValidationTourConfig& GetValidationTourConfig() const override {
return validationTour_;
}
const std::string& GetConfigJson() const override { return configJson_; }
private:
@@ -133,6 +136,7 @@ private:
sdl3cpp::services::GuiFontConfig guiFontConfig_{};
sdl3cpp::services::RenderBudgetConfig budgets_{};
sdl3cpp::services::CrashRecoveryConfig crashRecovery_{};
sdl3cpp::services::ValidationTourConfig validationTour_{};
std::string configJson_{};
};
@@ -167,6 +171,9 @@ public:
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 sdl3cpp::services::ValidationTourConfig& GetValidationTourConfig() const override {
return validationTour_;
}
const std::string& GetConfigJson() const override { return configJson_; }
private:
@@ -185,6 +192,7 @@ private:
sdl3cpp::services::GuiFontConfig guiFontConfig_{};
sdl3cpp::services::RenderBudgetConfig budgets_{};
sdl3cpp::services::CrashRecoveryConfig crashRecovery_{};
sdl3cpp::services::ValidationTourConfig validationTour_{};
std::string renderer_;
};