mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-26 22:54:59 +00:00
ROADMAP.md
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
329
src/services/impl/validation_tour_service.cpp
Normal file
329
src/services/impl/validation_tour_service.cpp
Normal 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
|
||||
55
src/services/impl/validation_tour_service.hpp
Normal file
55
src/services/impl/validation_tour_service.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
50
src/services/interfaces/i_validation_tour_service.hpp
Normal file
50
src/services/interfaces/i_validation_tour_service.hpp
Normal 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
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user