diff --git a/CMakeLists.txt b/CMakeLists.txt index 117cf82..2e48bba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/ROADMAP.md b/ROADMAP.md index 534a344..851be80 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. diff --git a/config/schema/runtime_config_v2.schema.json b/config/schema/runtime_config_v2.schema.json index 11c0af7..c18e9b1 100644 --- a/config/schema/runtime_config_v2.schema.json +++ b/config/schema/runtime_config_v2.schema.json @@ -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": { diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index 6a0b4e4..20940e3 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -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 #include @@ -228,6 +230,12 @@ void ServiceBasedApp::RegisterServices() { registry_.GetService()); auto configService = registry_.GetService(); + // Validation tour service (startup visual checks) + registry_.RegisterService( + configService, + registry_.GetService(), + registry_.GetService()); + // Render graph service (DAG build + scheduling) registry_.RegisterService( registry_.GetService(), @@ -357,7 +365,8 @@ void ServiceBasedApp::RegisterServices() { registry_.GetService(), registry_.GetService(), registry_.GetService(), - registry_.GetService()); + registry_.GetService(), + registry_.GetService()); // Application loop service registry_.RegisterService( diff --git a/src/services/impl/bgfx_graphics_backend.cpp b/src/services/impl/bgfx_graphics_backend.cpp index 0c38e65..e09649c 100644 --- a/src/services/impl/bgfx_graphics_backend.cpp +++ b/src/services/impl/bgfx_graphics_backend.cpp @@ -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()); diff --git a/src/services/impl/bgfx_graphics_backend.hpp b/src/services/impl/bgfx_graphics_backend.hpp index 55bf640..5f84017 100644 --- a/src/services/impl/bgfx_graphics_backend.hpp +++ b/src/services/impl/bgfx_graphics_backend.hpp @@ -9,6 +9,7 @@ #include "../../core/vertex.hpp" #include #include +#include #include #include #include @@ -49,6 +50,8 @@ public: GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer, uint32_t indexOffset, uint32_t indexCount, int32_t vertexOffset, const std::array& modelMatrix) override; + void RequestScreenshot(GraphicsDeviceHandle device, + const std::filesystem::path& outputPath) override; GraphicsDeviceHandle GetPhysicalDevice() const override; std::pair GetSwapchainExtent() const override; uint32_t GetSwapchainFormat() const override; diff --git a/src/services/impl/graphics_service.cpp b/src/services/impl/graphics_service.cpp index 8fdfb93..bd27d22 100644 --- a/src/services/impl/graphics_service.cpp +++ b/src/services/impl/graphics_service.cpp @@ -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"); diff --git a/src/services/impl/graphics_service.hpp b/src/services/impl/graphics_service.hpp index 7e537ec..6acebfb 100644 --- a/src/services/impl/graphics_service.hpp +++ b/src/services/impl/graphics_service.hpp @@ -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; diff --git a/src/services/impl/gxm_graphics_backend.cpp b/src/services/impl/gxm_graphics_backend.cpp index 6235623..b5bab63 100644 --- a/src/services/impl/gxm_graphics_backend.cpp +++ b/src/services/impl/gxm_graphics_backend.cpp @@ -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; diff --git a/src/services/impl/gxm_graphics_backend.hpp b/src/services/impl/gxm_graphics_backend.hpp index ff40a33..da2a4d8 100644 --- a/src/services/impl/gxm_graphics_backend.hpp +++ b/src/services/impl/gxm_graphics_backend.hpp @@ -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, diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index 9c20e63..6169724 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -1190,6 +1190,150 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr 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& 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(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(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(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(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(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(rawValue); + } + + config.validationTour.checkpoints.push_back(std::move(checkpoint)); + } + } + } + return config; } @@ -1353,6 +1497,59 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config, static_cast(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(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); diff --git a/src/services/impl/json_config_service.hpp b/src/services/impl/json_config_service.hpp index 040fd4d..046dc45 100644 --- a/src/services/impl/json_config_service.hpp +++ b/src/services/impl/json_config_service.hpp @@ -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"); diff --git a/src/services/impl/json_config_writer_service.cpp b/src/services/impl/json_config_writer_service.cpp index a10a4c9..8e662df 100644 --- a/src/services/impl/json_config_writer_service.cpp +++ b/src/services/impl/json_config_writer_service.cpp @@ -252,6 +252,59 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std static_cast(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(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); diff --git a/src/services/impl/render_coordinator_service.cpp b/src/services/impl/render_coordinator_service.cpp index 7582b46..f572ed1 100644 --- a/src/services/impl/render_coordinator_service.cpp +++ b/src/services/impl/render_coordinator_service.cpp @@ -1,6 +1,7 @@ #include "render_coordinator_service.hpp" #include +#include #include #include #include @@ -15,7 +16,8 @@ RenderCoordinatorService::RenderCoordinatorService(std::shared_ptr logg std::shared_ptr shaderScriptService, std::shared_ptr guiScriptService, std::shared_ptr guiService, - std::shared_ptr sceneService) + std::shared_ptr sceneService, + std::shared_ptr validationTourService) : logger_(std::move(logger)), configService_(std::move(configService)), configCompilerService_(std::move(configCompilerService)), @@ -24,7 +26,8 @@ RenderCoordinatorService::RenderCoordinatorService(std::shared_ptr 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 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(extent.first) / static_cast(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"); } diff --git a/src/services/impl/render_coordinator_service.hpp b/src/services/impl/render_coordinator_service.hpp index 25c61f0..abfb762 100644 --- a/src/services/impl/render_coordinator_service.hpp +++ b/src/services/impl/render_coordinator_service.hpp @@ -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 namespace sdl3cpp::services::impl { @@ -24,7 +25,8 @@ public: std::shared_ptr shaderScriptService, std::shared_ptr guiScriptService, std::shared_ptr guiService, - std::shared_ptr sceneService); + std::shared_ptr sceneService, + std::shared_ptr validationTourService); ~RenderCoordinatorService() override = default; void RenderFrame(float time) override; @@ -41,6 +43,7 @@ private: std::shared_ptr guiScriptService_; std::shared_ptr guiService_; std::shared_ptr sceneService_; + std::shared_ptr validationTourService_; size_t lastVertexCount_ = 0; size_t lastIndexCount_ = 0; bool shadersLoaded_ = false; diff --git a/src/services/impl/validation_tour_service.cpp b/src/services/impl/validation_tour_service.cpp new file mode 100644 index 0000000..2f65a16 --- /dev/null +++ b/src/services/impl/validation_tour_service.cpp @@ -0,0 +1,329 @@ +#include "validation_tour_service.hpp" + +#include "../interfaces/probe_types.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { +namespace { + +std::array ToArray(const glm::mat4& matrix) { + std::array result{}; + std::memcpy(result.data(), glm::value_ptr(matrix), sizeof(float) * result.size()); + return result; +} + +} + +ValidationTourService::ValidationTourService(std::shared_ptr configService, + std::shared_ptr probeService, + std::shared_ptr 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(actualWidth) * static_cast(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(std::abs(actual - expected)) / 255.0f; + totalDiff += static_cast(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(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 diff --git a/src/services/impl/validation_tour_service.hpp b/src/services/impl/validation_tour_service.hpp new file mode 100644 index 0000000..75c73e7 --- /dev/null +++ b/src/services/impl/validation_tour_service.hpp @@ -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 +#include + +namespace sdl3cpp::services::impl { + +class ValidationTourService final : public IValidationTourService { +public: + ValidationTourService(std::shared_ptr configService, + std::shared_ptr probeService, + std::shared_ptr 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 probeService_; + std::shared_ptr 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_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/interfaces/config_types.hpp b/src/services/interfaces/config_types.hpp index 8797d72..19b18b5 100644 --- a/src/services/interfaces/config_types.hpp +++ b/src/services/interfaces/config_types.hpp @@ -149,6 +149,48 @@ struct CrashRecoveryConfig { size_t maxMemoryWarnings = 3; }; +/** + * @brief Camera configuration for validation checkpoints. + */ +struct ValidationCameraConfig { + std::array position = {0.0f, 0.0f, 0.0f}; + std::array lookAt = {0.0f, 0.0f, -1.0f}; + std::array 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 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 diff --git a/src/services/interfaces/i_config_service.hpp b/src/services/interfaces/i_config_service.hpp index 8430d0c..a63af43 100644 --- a/src/services/interfaces/i_config_service.hpp +++ b/src/services/interfaces/i_config_service.hpp @@ -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. * diff --git a/src/services/interfaces/i_graphics_backend.hpp b/src/services/interfaces/i_graphics_backend.hpp index e384866..33e3c4b 100644 --- a/src/services/interfaces/i_graphics_backend.hpp +++ b/src/services/interfaces/i_graphics_backend.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -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. * diff --git a/src/services/interfaces/i_graphics_service.hpp b/src/services/interfaces/i_graphics_service.hpp index a2b07f9..40cc81b 100644 --- a/src/services/interfaces/i_graphics_service.hpp +++ b/src/services/interfaces/i_graphics_service.hpp @@ -5,6 +5,7 @@ #include "i_graphics_backend.hpp" #include #include +#include #include #include #include @@ -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. * diff --git a/src/services/interfaces/i_validation_tour_service.hpp b/src/services/interfaces/i_validation_tour_service.hpp new file mode 100644 index 0000000..6b5a111 --- /dev/null +++ b/src/services/interfaces/i_validation_tour_service.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "graphics_types.hpp" +#include +#include + +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 diff --git a/tests/bgfx_gui_budget_enforcement_test.cpp b/tests/bgfx_gui_budget_enforcement_test.cpp index a310b7d..255bff1 100644 --- a/tests/bgfx_gui_budget_enforcement_test.cpp +++ b/tests/bgfx_gui_budget_enforcement_test.cpp @@ -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_{}; }; diff --git a/tests/bgfx_texture_budget_tracker_test.cpp b/tests/bgfx_texture_budget_tracker_test.cpp index 913459d..c9005b8 100644 --- a/tests/bgfx_texture_budget_tracker_test.cpp +++ b/tests/bgfx_texture_budget_tracker_test.cpp @@ -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_{}; }; diff --git a/tests/graphics_service_buffer_lifecycle_test.cpp b/tests/graphics_service_buffer_lifecycle_test.cpp index 99d485f..5a1cb94 100644 --- a/tests/graphics_service_buffer_lifecycle_test.cpp +++ b/tests/graphics_service_buffer_lifecycle_test.cpp @@ -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, diff --git a/tests/render_coordinator_init_order_test.cpp b/tests/render_coordinator_init_order_test.cpp index 77939a7..58f5edd 100644 --- a/tests/render_coordinator_init_order_test.cpp +++ b/tests/render_coordinator_init_order_test.cpp @@ -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); diff --git a/tests/shader_system_registry_test.cpp b/tests/shader_system_registry_test.cpp index 6b63660..d3a21d6 100644 --- a/tests/shader_system_registry_test.cpp +++ b/tests/shader_system_registry_test.cpp @@ -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_; }; diff --git a/tests/test_bgfx_gui_service.cpp b/tests/test_bgfx_gui_service.cpp index 588591f..bcedda2 100644 --- a/tests/test_bgfx_gui_service.cpp +++ b/tests/test_bgfx_gui_service.cpp @@ -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_{}; }; diff --git a/tests/test_cube_script.cpp b/tests/test_cube_script.cpp index d3bca4e..9f69caf 100644 --- a/tests/test_cube_script.cpp +++ b/tests/test_cube_script.cpp @@ -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_; };