mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
feat: Implement configuration compiler and related services
- Added ConfigCompilerService to compile JSON configurations into IR structures. - Introduced IConfigCompilerService interface for compilation functionality. - Created ProbeService for structured diagnostics and reporting. - Developed RenderGraphService to build and validate render graphs. - Enhanced JsonConfigService to support schema validation and migration. - Introduced new interfaces for probing and rendering graph services. - Added necessary IR types for scenes, resources, and render passes. - Improved error handling and logging throughout the services.
This commit is contained in:
@@ -270,11 +270,13 @@ if(BUILD_SDL3_APP)
|
||||
src/di/service_registry.cpp
|
||||
src/events/event_bus.cpp
|
||||
src/services/impl/json_config_service.cpp
|
||||
src/services/impl/config_compiler_service.cpp
|
||||
src/services/impl/command_line_service.cpp
|
||||
src/services/impl/json_config_writer_service.cpp
|
||||
src/services/impl/logger_service.cpp
|
||||
src/services/impl/ecs_service.cpp
|
||||
src/services/impl/platform_service.cpp
|
||||
src/services/impl/probe_service.cpp
|
||||
src/services/impl/script_engine_service.cpp
|
||||
src/services/impl/lua_helpers.cpp
|
||||
src/services/impl/scene_script_service.cpp
|
||||
@@ -294,6 +296,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/render_graph_service.cpp
|
||||
src/services/impl/null_gui_service.cpp
|
||||
src/services/impl/bgfx_gui_service.cpp
|
||||
src/services/impl/bgfx_shader_compiler.cpp
|
||||
|
||||
38
ROADMAP.md
38
ROADMAP.md
@@ -17,21 +17,21 @@ Treat JSON config as a declarative control plane that compiles into scene, resou
|
||||
|
||||
### Starter Plan: "Bootstrap Hosting"
|
||||
- [x] Config version gating (`schema_version` / `configVersion` checks)
|
||||
- [ ] JSON Schema validation (external schema + validator)
|
||||
- [~] JSON-path diagnostics (path strings in exceptions, not full JSON Pointer coverage)
|
||||
- [x] JSON Schema validation (external schema + validator)
|
||||
- [~] JSON-path diagnostics (schema validator pointers + path strings; not full JSON Pointer coverage)
|
||||
- [~] Layered config merges (supports `extends` + `@delete`; no profile/local/CLI yet)
|
||||
- [x] Trace logging around config load, validation, and merge steps
|
||||
- [ ] Migration stubs for future versions
|
||||
- [~] Migration stubs for future versions (notes + stubbed hook)
|
||||
|
||||
### Pro Plan: "Graph Builder"
|
||||
- [ ] Typed IRs: `SceneIR`, `ResourceIR`, `RenderGraphIR`
|
||||
- [ ] Symbol tables + reference resolution with clear diagnostics
|
||||
- [ ] Render graph DAG compile with cycle detection
|
||||
- [ ] "Use before produce" validation for render pass inputs
|
||||
- [ ] Explicit pass scheduling and backend submission planning
|
||||
- [~] Typed IRs: `SceneIR`, `ResourceIR`, `RenderGraphIR`
|
||||
- [~] Symbol tables + reference resolution with clear diagnostics
|
||||
- [x] Render graph DAG compile with cycle detection
|
||||
- [x] "Use before produce" validation for render pass inputs
|
||||
- [~] Explicit pass scheduling and backend submission planning (schedule only)
|
||||
|
||||
### Ultra Plan: "Probe Fortress"
|
||||
- [ ] Probe hooks: `OnLoadScene`, `OnCreatePipeline`, `OnDraw`, `OnPresent`, `OnFrameEnd`
|
||||
- [~] Probe hooks: `OnLoadScene`, `OnCreatePipeline`, `OnDraw`, `OnPresent`, `OnFrameEnd`
|
||||
- [x] Pipeline compatibility checks (mesh layout vs shader inputs) via shader pipeline validator
|
||||
- [x] Sampler limits enforced from bgfx caps
|
||||
- [ ] Shader uniform compatibility enforcement
|
||||
@@ -51,13 +51,13 @@ Treat JSON config as a declarative control plane that compiles into scene, resou
|
||||
| Feature | Status | Starter | Pro | Ultra | Enterprise |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Config version gating (`schema_version` / `configVersion`) | Live | [x] | [ ] | [ ] | [ ] |
|
||||
| JSON Schema validation | Planned | [x] | [ ] | [ ] | [ ] |
|
||||
| JSON Schema validation | Live | [x] | [ ] | [ ] | [ ] |
|
||||
| Layered config merges + deterministic rules | Partial | [x] | [ ] | [ ] | [ ] |
|
||||
| JSON-path diagnostics | Partial | [x] | [ ] | [ ] | [ ] |
|
||||
| IR compilation (scene/resources/render) | Planned | [ ] | [x] | [ ] | [ ] |
|
||||
| Render graph DAG build + cycle checks | Planned | [ ] | [x] | [ ] | [ ] |
|
||||
| Pass scheduling + submission planning | Planned | [ ] | [x] | [ ] | [ ] |
|
||||
| Probe system + structured reports | Planned | [ ] | [ ] | [x] | [ ] |
|
||||
| IR compilation (scene/resources/render) | Partial | [ ] | [x] | [ ] | [ ] |
|
||||
| Render graph DAG build + cycle checks | Live | [ ] | [x] | [ ] | [ ] |
|
||||
| Pass scheduling + submission planning | Partial | [ ] | [x] | [ ] | [ ] |
|
||||
| Probe system + structured reports | Partial | [ ] | [ ] | [x] | [ ] |
|
||||
| Pipeline compatibility checks | Live | [ ] | [ ] | [x] | [ ] |
|
||||
| Sampler limits enforced | Live | [ ] | [ ] | [x] | [ ] |
|
||||
| Shader uniform compatibility enforcement | Planned | [ ] | [ ] | [x] | [ ] |
|
||||
@@ -68,10 +68,12 @@ Treat JSON config as a declarative control plane that compiles into scene, resou
|
||||
| Hot-reload + rollback | Planned | [ ] | [ ] | [ ] | [x] |
|
||||
|
||||
## Deliverables Checklist
|
||||
- [ ] `config/schema/` with versioned JSON Schema and migration notes
|
||||
- [ ] `src/services/impl/config_compiler_service.*` for JSON -> IR compilation
|
||||
- [ ] `src/services/impl/render_graph_service.*` for graph build and scheduling
|
||||
- [ ] `src/services/interfaces/i_probe_service.hpp` plus report/event types
|
||||
- [x] `config/schema/` with versioned JSON Schema and migration notes
|
||||
- [x] `src/services/impl/config_compiler_service.*` for JSON -> IR compilation
|
||||
- [x] `src/services/impl/render_graph_service.*` for graph build and scheduling
|
||||
- [x] `src/services/interfaces/i_probe_service.hpp` plus report/event types
|
||||
- [x] `src/services/impl/probe_service.*` for logging/queueing probe reports
|
||||
- [x] `src/services/interfaces/config_ir_types.hpp` for typed IR payloads
|
||||
- [~] Budget enforcement with clear failure modes and fallback resources (textures + GUI caches today)
|
||||
- [ ] Cube demo config-only boot path
|
||||
|
||||
|
||||
16
config/schema/MIGRATIONS.md
Normal file
16
config/schema/MIGRATIONS.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Config Schema Migrations
|
||||
|
||||
This folder tracks schema versions and how to migrate config JSON between them.
|
||||
|
||||
## Version 2 (current)
|
||||
- File: runtime_config_v2.schema.json
|
||||
- Supports the current runtime config shape plus optional render-graph sections.
|
||||
|
||||
## Stub: v2 -> v3
|
||||
When bumping to v3, add a migration step in `JsonConfigService::ApplyMigrations` that:
|
||||
- Detects `schema_version` or `configVersion` == 2
|
||||
- Transforms renamed or restructured fields into v3 layout
|
||||
- Emits trace logging for each field transformation
|
||||
- Updates `schema_version` and `configVersion` to 3
|
||||
|
||||
Add notes here for each structural change so the migration remains deterministic.
|
||||
288
config/schema/runtime_config_v2.schema.json
Normal file
288
config/schema/runtime_config_v2.schema.json
Normal file
@@ -0,0 +1,288 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "sdl3cpp.runtime.config.v2",
|
||||
"title": "SDL3 C++ Runtime Config v2",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"float3": {
|
||||
"type": "array",
|
||||
"items": {"type": "number"},
|
||||
"minItems": 3,
|
||||
"maxItems": 3
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"schema_version": {"type": "integer"},
|
||||
"configVersion": {"type": "integer"},
|
||||
"extends": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "array", "items": {"type": "string"}}
|
||||
]
|
||||
},
|
||||
"launcher": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"enabled": {"type": "boolean"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"scripts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entry": {"type": "string"},
|
||||
"lua_debug": {"type": "boolean"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"paths": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_root": {"type": "string"},
|
||||
"scripts": {"type": "string"},
|
||||
"shaders": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"window": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"size": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"width": {"type": "integer", "minimum": 0},
|
||||
"height": {"type": "integer", "minimum": 0}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"mouse_grab": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"grab_on_click": {"type": "boolean"},
|
||||
"release_on_escape": {"type": "boolean"},
|
||||
"start_grabbed": {"type": "boolean"},
|
||||
"hide_cursor": {"type": "boolean"},
|
||||
"relative_mode": {"type": "boolean"},
|
||||
"grab_mouse_button": {"type": "string"},
|
||||
"release_key": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bindings": {"type": "object", "additionalProperties": true}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"scene": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"rendering": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bgfx": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"renderer": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"materialx": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"parameters_enabled": {"type": "boolean"},
|
||||
"document": {"type": "string"},
|
||||
"shader_key": {"type": "string"},
|
||||
"material": {"type": "string"},
|
||||
"library_path": {"type": "string"},
|
||||
"library_folders": {"type": "array", "items": {"type": "string"}},
|
||||
"materials": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"document": {"type": "string"},
|
||||
"shader_key": {"type": "string"},
|
||||
"material": {"type": "string"},
|
||||
"use_constant_color": {"type": "boolean"},
|
||||
"constant_color": {"$ref": "#/definitions/float3"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"use_constant_color": {"type": "boolean"},
|
||||
"constant_color": {"$ref": "#/definitions/float3"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"atmospherics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ambient_strength": {"type": "number"},
|
||||
"fog_density": {"type": "number"},
|
||||
"fog_color": {"$ref": "#/definitions/float3"},
|
||||
"sky_color": {"$ref": "#/definitions/float3"},
|
||||
"gamma": {"type": "number"},
|
||||
"exposure": {"type": "number"},
|
||||
"enable_tone_mapping": {"type": "boolean"},
|
||||
"enable_shadows": {"type": "boolean"},
|
||||
"enable_ssgi": {"type": "boolean"},
|
||||
"enable_volumetric_lighting": {"type": "boolean"},
|
||||
"pbr_roughness": {"type": "number"},
|
||||
"pbr_metallic": {"type": "number"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"budgets": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vram_mb": {"type": "number", "minimum": 0},
|
||||
"max_texture_dim": {"type": "number", "minimum": 0},
|
||||
"gui_text_cache_entries": {"type": "number", "minimum": 0},
|
||||
"gui_svg_cache_entries": {"type": "number", "minimum": 0}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"crash_recovery": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"heartbeat_timeout_ms": {"type": "number", "minimum": 0},
|
||||
"heartbeat_poll_interval_ms": {"type": "number", "minimum": 0},
|
||||
"memory_limit_mb": {"type": "number", "minimum": 0},
|
||||
"gpu_hang_frame_time_multiplier": {"type": "number", "minimum": 0},
|
||||
"max_consecutive_gpu_timeouts": {"type": "number", "minimum": 0},
|
||||
"max_lua_failures": {"type": "number", "minimum": 0},
|
||||
"max_file_format_errors": {"type": "number", "minimum": 0},
|
||||
"max_memory_warnings": {"type": "number", "minimum": 0}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"gui": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"font": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"use_freetype": {"type": "boolean"},
|
||||
"font_path": {"type": "string"},
|
||||
"font_size": {"type": "number"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"opacity": {"type": "number"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"render": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"passes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"type": {"type": "string"},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@delete": {"type": "array", "items": {"type": "string"}}
|
||||
},
|
||||
"additionalProperties": {"type": "string"}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@delete": {"type": "array", "items": {"type": "string"}}
|
||||
},
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": {"type": "string"},
|
||||
"usage": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"parameters": {"type": "object", "additionalProperties": true}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"materialx": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"document": {"type": "string"},
|
||||
"shader_key": {"type": "string"},
|
||||
"material": {"type": "string"},
|
||||
"library_path": {"type": "string"},
|
||||
"library_folders": {"type": "array", "items": {"type": "string"}},
|
||||
"materials": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"document": {"type": "string"},
|
||||
"shader_key": {"type": "string"},
|
||||
"material": {"type": "string"},
|
||||
"use_constant_color": {"type": "boolean"},
|
||||
"constant_color": {"$ref": "#/definitions/float3"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"use_constant_color": {"type": "boolean"},
|
||||
"constant_color": {"$ref": "#/definitions/float3"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"materialx_materials": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"document": {"type": "string"},
|
||||
"shader_key": {"type": "string"},
|
||||
"material": {"type": "string"},
|
||||
"use_constant_color": {"type": "boolean"},
|
||||
"constant_color": {"$ref": "#/definitions/float3"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"config_file": {"type": "string"},
|
||||
"lua_script": {"type": "string"},
|
||||
"window_width": {"type": "integer", "minimum": 0},
|
||||
"window_height": {"type": "integer", "minimum": 0},
|
||||
"input_bindings": {"type": "object", "additionalProperties": true},
|
||||
"mouse_grab": {"type": "object", "additionalProperties": true},
|
||||
"gui_font": {"type": "object", "additionalProperties": true},
|
||||
"gui_opacity": {"type": "number"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
@@ -6,10 +6,13 @@
|
||||
#include "services/interfaces/i_application_loop_service.hpp"
|
||||
#include "services/interfaces/i_lifecycle_service.hpp"
|
||||
#include "services/impl/json_config_service.hpp"
|
||||
#include "services/impl/config_compiler_service.hpp"
|
||||
#include "services/impl/lifecycle_service.hpp"
|
||||
#include "services/impl/application_loop_service.hpp"
|
||||
#include "services/impl/render_coordinator_service.hpp"
|
||||
#include "services/impl/render_graph_service.hpp"
|
||||
#include "services/impl/platform_service.hpp"
|
||||
#include "services/impl/probe_service.hpp"
|
||||
#include "services/impl/sdl_window_service.hpp"
|
||||
#include "services/impl/sdl_input_service.hpp"
|
||||
#include "services/impl/ecs_service.hpp"
|
||||
@@ -33,6 +36,9 @@
|
||||
#include "services/impl/logger_service.hpp"
|
||||
#include "services/impl/pipeline_compiler_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_config_compiler_service.hpp"
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
@@ -209,10 +215,28 @@ void ServiceBasedApp::RegisterServices() {
|
||||
// Event bus (needed by window service)
|
||||
registry_.RegisterService<events::IEventBus, events::EventBus>();
|
||||
|
||||
// Probe service (structured diagnostics)
|
||||
registry_.RegisterService<services::IProbeService, services::impl::ProbeService>(
|
||||
registry_.GetService<services::ILogger>());
|
||||
|
||||
// Configuration service
|
||||
registry_.RegisterService<services::IConfigService, services::impl::JsonConfigService>(
|
||||
registry_.GetService<services::ILogger>(), runtimeConfig_);
|
||||
registry_.GetService<services::ILogger>(),
|
||||
runtimeConfig_,
|
||||
registry_.GetService<services::IProbeService>());
|
||||
auto configService = registry_.GetService<services::IConfigService>();
|
||||
|
||||
// Render graph service (DAG build + scheduling)
|
||||
registry_.RegisterService<services::IRenderGraphService, services::impl::RenderGraphService>(
|
||||
registry_.GetService<services::ILogger>(),
|
||||
registry_.GetService<services::IProbeService>());
|
||||
|
||||
// Config compiler service (JSON -> IR)
|
||||
registry_.RegisterService<services::IConfigCompilerService, services::impl::ConfigCompilerService>(
|
||||
registry_.GetService<services::IConfigService>(),
|
||||
registry_.GetService<services::IRenderGraphService>(),
|
||||
registry_.GetService<services::IProbeService>(),
|
||||
registry_.GetService<services::ILogger>());
|
||||
// ECS service (entt registry)
|
||||
registry_.RegisterService<services::IEcsService, services::impl::EcsService>(
|
||||
registry_.GetService<services::ILogger>());
|
||||
@@ -283,7 +307,8 @@ void ServiceBasedApp::RegisterServices() {
|
||||
registry_.GetService<services::IConfigService>(),
|
||||
registry_.GetService<services::IPlatformService>(),
|
||||
registry_.GetService<services::ILogger>(),
|
||||
registry_.GetService<services::IPipelineCompilerService>());
|
||||
registry_.GetService<services::IPipelineCompilerService>(),
|
||||
registry_.GetService<services::IProbeService>());
|
||||
|
||||
// Graphics service (facade)
|
||||
registry_.RegisterService<services::IGraphicsService, services::impl::GraphicsService>(
|
||||
|
||||
@@ -275,11 +275,13 @@ std::optional<bgfx::RendererType::Enum> RecommendFallbackRenderer(
|
||||
BgfxGraphicsBackend::BgfxGraphicsBackend(std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<IPlatformService> platformService,
|
||||
std::shared_ptr<ILogger> logger,
|
||||
std::shared_ptr<IPipelineCompilerService> pipelineCompiler)
|
||||
std::shared_ptr<IPipelineCompilerService> pipelineCompiler,
|
||||
std::shared_ptr<IProbeService> probeService)
|
||||
: configService_(std::move(configService)),
|
||||
platformService_(std::move(platformService)),
|
||||
logger_(std::move(logger)),
|
||||
pipelineCompiler_(std::move(pipelineCompiler)) {
|
||||
pipelineCompiler_(std::move(pipelineCompiler)),
|
||||
probeService_(std::move(probeService)) {
|
||||
if (logger_) {
|
||||
logger_->Trace("BgfxGraphicsBackend", "BgfxGraphicsBackend",
|
||||
"configService=" + std::string(configService_ ? "set" : "null") +
|
||||
@@ -1178,11 +1180,23 @@ void BgfxGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHand
|
||||
GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer,
|
||||
uint32_t indexOffset, uint32_t indexCount, int32_t vertexOffset,
|
||||
const std::array<float, 16>& modelMatrix) {
|
||||
auto reportError = [&](const std::string& code, const std::string& message) {
|
||||
if (!probeService_) {
|
||||
return;
|
||||
}
|
||||
ProbeReport report{};
|
||||
report.severity = ProbeSeverity::Error;
|
||||
report.code = code;
|
||||
report.message = message;
|
||||
probeService_->Report(report);
|
||||
};
|
||||
|
||||
auto pipelineIt = pipelines_.find(pipeline);
|
||||
if (pipelineIt == pipelines_.end()) {
|
||||
if (logger_) {
|
||||
logger_->Error("BgfxGraphicsBackend::Draw: Pipeline not found");
|
||||
}
|
||||
reportError("DRAW_PIPELINE_MISSING", "Draw call missing pipeline");
|
||||
return;
|
||||
}
|
||||
auto vertexIt = vertexBuffers_.find(vertexBuffer);
|
||||
@@ -1191,6 +1205,7 @@ void BgfxGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHand
|
||||
if (logger_) {
|
||||
logger_->Error("BgfxGraphicsBackend::Draw: Buffer handles not found");
|
||||
}
|
||||
reportError("DRAW_BUFFER_MISSING", "Draw call missing vertex or index buffer");
|
||||
return;
|
||||
}
|
||||
const auto& vb = vertexIt->second;
|
||||
@@ -1211,6 +1226,7 @@ void BgfxGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHand
|
||||
logger_->Error("BgfxGraphicsBackend::Draw: Invalid negative vertex offset (" +
|
||||
std::to_string(vertexOffset) + ")");
|
||||
}
|
||||
reportError("DRAW_VERTEX_OFFSET_NEGATIVE", "Draw call has negative vertex offset");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1220,6 +1236,7 @@ void BgfxGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHand
|
||||
std::to_string(vertexOffset) + ") exceeds vertex buffer size (" +
|
||||
std::to_string(vb->vertexCount) + ")");
|
||||
}
|
||||
reportError("DRAW_VERTEX_OFFSET_RANGE", "Draw call vertex offset exceeds vertex buffer size");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1231,6 +1248,7 @@ void BgfxGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHand
|
||||
") exceeds index buffer size (" +
|
||||
std::to_string(ib->indexCount) + ")");
|
||||
}
|
||||
reportError("DRAW_INDEX_RANGE", "Draw call index range exceeds index buffer size");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include "../interfaces/i_platform_service.hpp"
|
||||
#include "../interfaces/i_pipeline_compiler_service.hpp"
|
||||
#include "../interfaces/i_probe_service.hpp"
|
||||
#include "../../core/vertex.hpp"
|
||||
#include <bgfx/bgfx.h>
|
||||
#include <array>
|
||||
@@ -19,7 +20,8 @@ public:
|
||||
BgfxGraphicsBackend(std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<IPlatformService> platformService,
|
||||
std::shared_ptr<ILogger> logger,
|
||||
std::shared_ptr<IPipelineCompilerService> pipelineCompiler);
|
||||
std::shared_ptr<IPipelineCompilerService> pipelineCompiler,
|
||||
std::shared_ptr<IProbeService> probeService = nullptr);
|
||||
~BgfxGraphicsBackend() override;
|
||||
|
||||
void Initialize(void* window, const GraphicsConfig& config) override;
|
||||
@@ -157,6 +159,7 @@ private:
|
||||
std::shared_ptr<IPlatformService> platformService_;
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
std::shared_ptr<IPipelineCompilerService> pipelineCompiler_;
|
||||
std::shared_ptr<IProbeService> probeService_;
|
||||
bgfx::VertexLayout vertexLayout_;
|
||||
std::unordered_map<GraphicsPipelineHandle, std::unique_ptr<PipelineEntry>> pipelines_;
|
||||
std::unordered_map<GraphicsBufferHandle, std::unique_ptr<VertexBufferEntry>> vertexBuffers_;
|
||||
|
||||
579
src/services/impl/config_compiler_service.cpp
Normal file
579
src/services/impl/config_compiler_service.cpp
Normal file
@@ -0,0 +1,579 @@
|
||||
#include "config_compiler_service.hpp"
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/error/en.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsErrorSeverity(ProbeSeverity severity) {
|
||||
return severity == ProbeSeverity::Error || severity == ProbeSeverity::Fatal;
|
||||
}
|
||||
|
||||
std::string JoinPath(const std::string& base, const std::string& segment) {
|
||||
if (base.empty()) {
|
||||
return "/" + segment;
|
||||
}
|
||||
return base + "/" + segment;
|
||||
}
|
||||
|
||||
bool ParsePassOutputReference(const std::string& value, std::string& passId, std::string& outputName) {
|
||||
const std::string prefix = "@pass.";
|
||||
if (value.rfind(prefix, 0) != 0) {
|
||||
return false;
|
||||
}
|
||||
const std::string remainder = value.substr(prefix.size());
|
||||
const size_t dot = remainder.find('.');
|
||||
if (dot == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
passId = remainder.substr(0, dot);
|
||||
outputName = remainder.substr(dot + 1);
|
||||
return !passId.empty() && !outputName.empty();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConfigCompilerService::ConfigCompilerService(std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<IRenderGraphService> renderGraphService,
|
||||
std::shared_ptr<IProbeService> probeService,
|
||||
std::shared_ptr<ILogger> logger)
|
||||
: configService_(std::move(configService)),
|
||||
renderGraphService_(std::move(renderGraphService)),
|
||||
probeService_(std::move(probeService)),
|
||||
logger_(std::move(logger)) {
|
||||
if (logger_) {
|
||||
logger_->Trace("ConfigCompilerService", "ConfigCompilerService", "initialized=true");
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigCompilerService::Initialize() {
|
||||
if (!configService_) {
|
||||
throw std::runtime_error("ConfigCompilerService requires a config service");
|
||||
}
|
||||
const std::string configJson = configService_->GetConfigJson();
|
||||
if (configJson.empty()) {
|
||||
if (logger_) {
|
||||
logger_->Warn("ConfigCompilerService::Initialize: Config JSON is empty; skipping compile");
|
||||
}
|
||||
return;
|
||||
}
|
||||
lastResult_ = Compile(configJson);
|
||||
if (logger_) {
|
||||
const std::string status = lastResult_.success ? "success" : "errors";
|
||||
logger_->Info("ConfigCompilerService::Initialize: Config compile " + status +
|
||||
" (diagnostics=" + std::to_string(lastResult_.diagnostics.size()) + ")");
|
||||
}
|
||||
}
|
||||
|
||||
ConfigCompilerResult ConfigCompilerService::Compile(const std::string& configJson) {
|
||||
ConfigCompilerResult result;
|
||||
result.success = true;
|
||||
|
||||
rapidjson::Document document;
|
||||
document.Parse(configJson.c_str());
|
||||
if (document.HasParseError()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"CONFIG_JSON_PARSE",
|
||||
"",
|
||||
std::string("JSON parse error: ") + rapidjson::GetParseError_En(document.GetParseError()),
|
||||
"offset=" + std::to_string(document.GetErrorOffset()));
|
||||
return result;
|
||||
}
|
||||
if (!document.IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"CONFIG_JSON_ROOT",
|
||||
"",
|
||||
"JSON root must be an object");
|
||||
return result;
|
||||
}
|
||||
|
||||
auto getObjectMember = [&](const rapidjson::Value& parent,
|
||||
const char* name,
|
||||
const std::string& path) -> const rapidjson::Value* {
|
||||
if (!parent.HasMember(name)) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& value = parent[name];
|
||||
if (!value.IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"CONFIG_JSON_TYPE",
|
||||
path,
|
||||
std::string("Expected object for ") + name);
|
||||
return nullptr;
|
||||
}
|
||||
return &value;
|
||||
};
|
||||
|
||||
auto getArrayMember = [&](const rapidjson::Value& parent,
|
||||
const char* name,
|
||||
const std::string& path) -> const rapidjson::Value* {
|
||||
if (!parent.HasMember(name)) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& value = parent[name];
|
||||
if (!value.IsArray()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"CONFIG_JSON_TYPE",
|
||||
path,
|
||||
std::string("Expected array for ") + name);
|
||||
return nullptr;
|
||||
}
|
||||
return &value;
|
||||
};
|
||||
|
||||
const std::string scenePath = "/scene";
|
||||
if (const auto* sceneValue = getObjectMember(document, "scene", scenePath)) {
|
||||
const std::string entitiesPath = JoinPath(scenePath, "entities");
|
||||
if (const auto* entitiesValue = getArrayMember(*sceneValue, "entities", entitiesPath)) {
|
||||
for (rapidjson::SizeType i = 0; i < entitiesValue->Size(); ++i) {
|
||||
const auto& entry = (*entitiesValue)[i];
|
||||
const std::string entityPath = entitiesPath + "/" + std::to_string(i);
|
||||
if (!entry.IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"SCENE_ENTITY_TYPE",
|
||||
entityPath,
|
||||
"Scene entity must be an object");
|
||||
continue;
|
||||
}
|
||||
SceneEntityIR entity;
|
||||
entity.jsonPath = entityPath;
|
||||
if (entry.HasMember("id") && entry["id"].IsString()) {
|
||||
entity.id = entry["id"].GetString();
|
||||
} else {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"SCENE_ENTITY_ID_MISSING",
|
||||
entityPath,
|
||||
"Scene entity requires a string id");
|
||||
continue;
|
||||
}
|
||||
if (entry.HasMember("components")) {
|
||||
if (!entry["components"].IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"SCENE_COMPONENTS_TYPE",
|
||||
JoinPath(entityPath, "components"),
|
||||
"Scene components must be an object");
|
||||
} else {
|
||||
const auto& components = entry["components"];
|
||||
for (auto it = components.MemberBegin(); it != components.MemberEnd(); ++it) {
|
||||
if (it->name.IsString()) {
|
||||
entity.componentTypes.emplace_back(it->name.GetString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.scene.entities.push_back(std::move(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string assetsPath = "/assets";
|
||||
std::unordered_set<std::string> textureIds;
|
||||
std::unordered_set<std::string> shaderIds;
|
||||
if (const auto* assetsValue = getObjectMember(document, "assets", assetsPath)) {
|
||||
if (const auto* texturesValue = getObjectMember(*assetsValue, "textures", JoinPath(assetsPath, "textures"))) {
|
||||
for (auto it = texturesValue->MemberBegin(); it != texturesValue->MemberEnd(); ++it) {
|
||||
const std::string id = it->name.GetString();
|
||||
const std::string texturePath = JoinPath(JoinPath(assetsPath, "textures"), id);
|
||||
if (!it->value.IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"ASSET_TEXTURE_TYPE",
|
||||
texturePath,
|
||||
"Texture entry must be an object");
|
||||
continue;
|
||||
}
|
||||
if (!it->value.HasMember("uri") || !it->value["uri"].IsString()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"ASSET_TEXTURE_URI",
|
||||
JoinPath(texturePath, "uri"),
|
||||
"Texture entry requires a string uri");
|
||||
continue;
|
||||
}
|
||||
TextureIR texture;
|
||||
texture.id = id;
|
||||
texture.uri = it->value["uri"].GetString();
|
||||
texture.jsonPath = texturePath;
|
||||
result.resources.textures.push_back(std::move(texture));
|
||||
textureIds.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto* meshesValue = getObjectMember(*assetsValue, "meshes", JoinPath(assetsPath, "meshes"))) {
|
||||
for (auto it = meshesValue->MemberBegin(); it != meshesValue->MemberEnd(); ++it) {
|
||||
const std::string id = it->name.GetString();
|
||||
const std::string meshPath = JoinPath(JoinPath(assetsPath, "meshes"), id);
|
||||
if (!it->value.IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"ASSET_MESH_TYPE",
|
||||
meshPath,
|
||||
"Mesh entry must be an object");
|
||||
continue;
|
||||
}
|
||||
if (!it->value.HasMember("uri") || !it->value["uri"].IsString()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"ASSET_MESH_URI",
|
||||
JoinPath(meshPath, "uri"),
|
||||
"Mesh entry requires a string uri");
|
||||
continue;
|
||||
}
|
||||
MeshIR mesh;
|
||||
mesh.id = id;
|
||||
mesh.uri = it->value["uri"].GetString();
|
||||
mesh.jsonPath = meshPath;
|
||||
result.resources.meshes.push_back(std::move(mesh));
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto* shadersValue = getObjectMember(*assetsValue, "shaders", JoinPath(assetsPath, "shaders"))) {
|
||||
for (auto it = shadersValue->MemberBegin(); it != shadersValue->MemberEnd(); ++it) {
|
||||
const std::string id = it->name.GetString();
|
||||
const std::string shaderPath = JoinPath(JoinPath(assetsPath, "shaders"), id);
|
||||
if (!it->value.IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"ASSET_SHADER_TYPE",
|
||||
shaderPath,
|
||||
"Shader entry must be an object");
|
||||
continue;
|
||||
}
|
||||
if (!it->value.HasMember("vs") || !it->value["vs"].IsString() ||
|
||||
!it->value.HasMember("fs") || !it->value["fs"].IsString()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"ASSET_SHADER_PATHS",
|
||||
shaderPath,
|
||||
"Shader entry requires vs and fs string paths");
|
||||
continue;
|
||||
}
|
||||
ShaderIR shader;
|
||||
shader.id = id;
|
||||
shader.vertexPath = it->value["vs"].GetString();
|
||||
shader.fragmentPath = it->value["fs"].GetString();
|
||||
shader.jsonPath = shaderPath;
|
||||
result.resources.shaders.push_back(std::move(shader));
|
||||
shaderIds.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string materialsPath = "/materials";
|
||||
if (const auto* materialsValue = getObjectMember(document, "materials", materialsPath)) {
|
||||
for (auto it = materialsValue->MemberBegin(); it != materialsValue->MemberEnd(); ++it) {
|
||||
const std::string id = it->name.GetString();
|
||||
const std::string materialPath = JoinPath(materialsPath, id);
|
||||
if (!it->value.IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"MATERIAL_TYPE",
|
||||
materialPath,
|
||||
"Material entry must be an object");
|
||||
continue;
|
||||
}
|
||||
if (!it->value.HasMember("shader") || !it->value["shader"].IsString()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"MATERIAL_SHADER",
|
||||
JoinPath(materialPath, "shader"),
|
||||
"Material requires a shader id");
|
||||
continue;
|
||||
}
|
||||
MaterialIR material;
|
||||
material.id = id;
|
||||
material.shader = it->value["shader"].GetString();
|
||||
material.jsonPath = materialPath;
|
||||
|
||||
if (!shaderIds.empty() && shaderIds.find(material.shader) == shaderIds.end()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"MATERIAL_SHADER_UNKNOWN",
|
||||
JoinPath(materialPath, "shader"),
|
||||
"Material references unknown shader: " + material.shader);
|
||||
}
|
||||
|
||||
if (it->value.HasMember("textures")) {
|
||||
const auto& texturesValue = it->value["textures"];
|
||||
if (!texturesValue.IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"MATERIAL_TEXTURES_TYPE",
|
||||
JoinPath(materialPath, "textures"),
|
||||
"Material textures must be an object");
|
||||
} else {
|
||||
for (auto texIt = texturesValue.MemberBegin(); texIt != texturesValue.MemberEnd(); ++texIt) {
|
||||
const std::string uniform = texIt->name.GetString();
|
||||
if (!texIt->value.IsString()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"MATERIAL_TEXTURE_REF",
|
||||
JoinPath(JoinPath(materialPath, "textures"), uniform),
|
||||
"Material texture mapping must be a string id");
|
||||
continue;
|
||||
}
|
||||
const std::string textureId = texIt->value.GetString();
|
||||
material.textures.emplace(uniform, textureId);
|
||||
if (!textureIds.empty() && textureIds.find(textureId) == textureIds.end()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"MATERIAL_TEXTURE_UNKNOWN",
|
||||
JoinPath(JoinPath(materialPath, "textures"), uniform),
|
||||
"Material references unknown texture: " + textureId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.resources.materials.push_back(std::move(material));
|
||||
}
|
||||
}
|
||||
|
||||
const std::string renderPath = "/render";
|
||||
if (const auto* renderValue = getObjectMember(document, "render", renderPath)) {
|
||||
const std::string passesPath = JoinPath(renderPath, "passes");
|
||||
if (const auto* passesValue = getArrayMember(*renderValue, "passes", passesPath)) {
|
||||
std::unordered_map<std::string, std::unordered_set<std::string>> outputsByPass;
|
||||
for (rapidjson::SizeType i = 0; i < passesValue->Size(); ++i) {
|
||||
const auto& passValue = (*passesValue)[i];
|
||||
const std::string passPath = passesPath + "/" + std::to_string(i);
|
||||
if (!passValue.IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_PASS_TYPE",
|
||||
passPath,
|
||||
"Render pass must be an object");
|
||||
continue;
|
||||
}
|
||||
RenderPassIR pass;
|
||||
pass.jsonPath = passPath;
|
||||
if (passValue.HasMember("id") && passValue["id"].IsString()) {
|
||||
pass.id = passValue["id"].GetString();
|
||||
} else {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_PASS_ID",
|
||||
JoinPath(passPath, "id"),
|
||||
"Render pass requires a string id");
|
||||
continue;
|
||||
}
|
||||
if (passValue.HasMember("type") && passValue["type"].IsString()) {
|
||||
pass.type = passValue["type"].GetString();
|
||||
}
|
||||
outputsByPass.emplace(pass.id, std::unordered_set<std::string>{});
|
||||
|
||||
if (passValue.HasMember("inputs")) {
|
||||
const auto& inputsValue = passValue["inputs"];
|
||||
if (!inputsValue.IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_INPUTS_TYPE",
|
||||
JoinPath(passPath, "inputs"),
|
||||
"Render pass inputs must be an object");
|
||||
} else {
|
||||
for (auto inputIt = inputsValue.MemberBegin(); inputIt != inputsValue.MemberEnd(); ++inputIt) {
|
||||
const std::string inputName = inputIt->name.GetString();
|
||||
const std::string inputPath = JoinPath(JoinPath(passPath, "inputs"), inputName);
|
||||
if (!inputIt->value.IsString()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_INPUT_REF",
|
||||
inputPath,
|
||||
"Render pass input must be a string reference");
|
||||
continue;
|
||||
}
|
||||
const std::string inputValue = inputIt->value.GetString();
|
||||
RenderPassInputIR input;
|
||||
input.name = inputName;
|
||||
input.jsonPath = inputPath;
|
||||
if (inputValue == "@swapchain") {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_INPUT_SWAPCHAIN",
|
||||
inputPath,
|
||||
"Render pass input cannot reference @swapchain");
|
||||
} else {
|
||||
std::string passId;
|
||||
std::string outputName;
|
||||
if (ParsePassOutputReference(inputValue, passId, outputName)) {
|
||||
input.ref.type = RenderResourceRefType::PassOutput;
|
||||
input.ref.passId = passId;
|
||||
input.ref.outputName = outputName;
|
||||
input.ref.jsonPath = inputPath;
|
||||
} else {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_INPUT_REF_FORMAT",
|
||||
inputPath,
|
||||
"Render pass input must use @pass.<id>.<output>");
|
||||
}
|
||||
}
|
||||
pass.inputs.push_back(std::move(input));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (passValue.HasMember("outputs")) {
|
||||
const auto& outputsValue = passValue["outputs"];
|
||||
if (!outputsValue.IsObject()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_OUTPUTS_TYPE",
|
||||
JoinPath(passPath, "outputs"),
|
||||
"Render pass outputs must be an object");
|
||||
} else {
|
||||
for (auto outputIt = outputsValue.MemberBegin(); outputIt != outputsValue.MemberEnd(); ++outputIt) {
|
||||
const std::string outputName = outputIt->name.GetString();
|
||||
const std::string outputPath = JoinPath(JoinPath(passPath, "outputs"), outputName);
|
||||
RenderPassOutputIR output;
|
||||
output.name = outputName;
|
||||
output.jsonPath = outputPath;
|
||||
bool outputValid = true;
|
||||
if (outputIt->value.IsString()) {
|
||||
const std::string outputValue = outputIt->value.GetString();
|
||||
if (outputValue == "@swapchain") {
|
||||
output.isSwapchain = true;
|
||||
} else {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_OUTPUT_REF",
|
||||
outputPath,
|
||||
"Render pass output must be an object or @swapchain");
|
||||
outputValid = false;
|
||||
}
|
||||
} else if (outputIt->value.IsObject()) {
|
||||
const auto& outputObject = outputIt->value;
|
||||
if (outputObject.HasMember("format") && outputObject["format"].IsString()) {
|
||||
output.format = outputObject["format"].GetString();
|
||||
}
|
||||
if (outputObject.HasMember("usage") && outputObject["usage"].IsString()) {
|
||||
output.usage = outputObject["usage"].GetString();
|
||||
}
|
||||
} else {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_OUTPUT_TYPE",
|
||||
outputPath,
|
||||
"Render pass output must be an object or string");
|
||||
outputValid = false;
|
||||
}
|
||||
if (outputValid) {
|
||||
pass.outputs.push_back(std::move(output));
|
||||
if (!pass.id.empty() && !output.isSwapchain) {
|
||||
outputsByPass[pass.id].insert(outputName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.renderGraph.passes.push_back(std::move(pass));
|
||||
}
|
||||
|
||||
for (const auto& pass : result.renderGraph.passes) {
|
||||
for (const auto& input : pass.inputs) {
|
||||
if (input.ref.type != RenderResourceRefType::PassOutput) {
|
||||
continue;
|
||||
}
|
||||
if (input.ref.passId == pass.id) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_INPUT_SELF_REFERENCE",
|
||||
input.jsonPath,
|
||||
"Render pass input references its own output");
|
||||
continue;
|
||||
}
|
||||
auto passIt = outputsByPass.find(input.ref.passId);
|
||||
if (passIt == outputsByPass.end()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_INPUT_UNKNOWN_PASS",
|
||||
input.jsonPath,
|
||||
"Render pass input references unknown pass: " + input.ref.passId);
|
||||
continue;
|
||||
}
|
||||
if (!input.ref.outputName.empty() &&
|
||||
passIt->second.find(input.ref.outputName) == passIt->second.end()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_INPUT_UNKNOWN_OUTPUT",
|
||||
input.jsonPath,
|
||||
"Render pass input references unknown output: " + input.ref.outputName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (renderGraphService_) {
|
||||
const auto buildResult = renderGraphService_->BuildGraph(result.renderGraph);
|
||||
MergeRenderGraphDiagnostics(result, buildResult);
|
||||
}
|
||||
|
||||
if (logger_) {
|
||||
logger_->Trace("ConfigCompilerService", "Compile",
|
||||
"diagnostics=" + std::to_string(result.diagnostics.size()) +
|
||||
", success=" + std::string(result.success ? "true" : "false"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ConfigCompilerService::AddDiagnostic(ConfigCompilerResult& result,
|
||||
ProbeSeverity severity,
|
||||
const std::string& code,
|
||||
const std::string& jsonPath,
|
||||
const std::string& message,
|
||||
const std::string& details) {
|
||||
ProbeReport report;
|
||||
report.severity = severity;
|
||||
report.code = code;
|
||||
report.jsonPath = jsonPath;
|
||||
report.message = message;
|
||||
report.details = details;
|
||||
result.diagnostics.push_back(report);
|
||||
if (IsErrorSeverity(severity)) {
|
||||
result.success = false;
|
||||
}
|
||||
if (probeService_) {
|
||||
probeService_->Report(report);
|
||||
}
|
||||
if (logger_) {
|
||||
if (severity == ProbeSeverity::Warn) {
|
||||
logger_->Warn("ConfigCompilerService::Compile: " + message + " (" + code + ")");
|
||||
} else if (IsErrorSeverity(severity)) {
|
||||
logger_->Error("ConfigCompilerService::Compile: " + message + " (" + code + ")");
|
||||
} else {
|
||||
logger_->Info("ConfigCompilerService::Compile: " + message + " (" + code + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigCompilerService::MergeRenderGraphDiagnostics(ConfigCompilerResult& result,
|
||||
const RenderGraphBuildResult& renderGraphBuild) {
|
||||
result.renderGraphBuild = renderGraphBuild;
|
||||
for (const auto& report : renderGraphBuild.diagnostics) {
|
||||
result.diagnostics.push_back(report);
|
||||
if (IsErrorSeverity(report.severity)) {
|
||||
result.success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
42
src/services/impl/config_compiler_service.hpp
Normal file
42
src/services/impl/config_compiler_service.hpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "../interfaces/i_config_compiler_service.hpp"
|
||||
#include "../interfaces/i_config_service.hpp"
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include "../interfaces/i_probe_service.hpp"
|
||||
#include "../interfaces/i_render_graph_service.hpp"
|
||||
#include "../../di/lifecycle.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
class ConfigCompilerService : public IConfigCompilerService, public di::IInitializable {
|
||||
public:
|
||||
ConfigCompilerService(std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<IRenderGraphService> renderGraphService,
|
||||
std::shared_ptr<IProbeService> probeService,
|
||||
std::shared_ptr<ILogger> logger);
|
||||
|
||||
void Initialize() override;
|
||||
|
||||
ConfigCompilerResult Compile(const std::string& configJson) override;
|
||||
const ConfigCompilerResult& GetLastResult() const override { return lastResult_; }
|
||||
|
||||
private:
|
||||
void AddDiagnostic(ConfigCompilerResult& result,
|
||||
ProbeSeverity severity,
|
||||
const std::string& code,
|
||||
const std::string& jsonPath,
|
||||
const std::string& message,
|
||||
const std::string& details = "");
|
||||
void MergeRenderGraphDiagnostics(ConfigCompilerResult& result,
|
||||
const RenderGraphBuildResult& renderGraphBuild);
|
||||
|
||||
std::shared_ptr<IConfigService> configService_;
|
||||
std::shared_ptr<IRenderGraphService> renderGraphService_;
|
||||
std::shared_ptr<IProbeService> probeService_;
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
ConfigCompilerResult lastResult_{};
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "json_config_service.hpp"
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/error/en.h>
|
||||
#include <rapidjson/istreamwrapper.h>
|
||||
#include <rapidjson/schema.h>
|
||||
#include <rapidjson/stringbuffer.h>
|
||||
#include <rapidjson/writer.h>
|
||||
#include <rapidjson/prettywriter.h>
|
||||
@@ -33,20 +35,22 @@ std::filesystem::path NormalizeConfigPath(const std::filesystem::path& path) {
|
||||
return canonicalPath;
|
||||
}
|
||||
|
||||
rapidjson::Document ParseJsonDocument(const std::filesystem::path& configPath) {
|
||||
std::ifstream configStream(configPath);
|
||||
rapidjson::Document ParseJsonDocument(const std::filesystem::path& jsonPath,
|
||||
const std::string& description) {
|
||||
std::ifstream configStream(jsonPath);
|
||||
if (!configStream) {
|
||||
throw std::runtime_error("Failed to open config file: " + configPath.string());
|
||||
throw std::runtime_error("Failed to open " + description + ": " + jsonPath.string());
|
||||
}
|
||||
|
||||
rapidjson::IStreamWrapper inputWrapper(configStream);
|
||||
rapidjson::Document document;
|
||||
document.ParseStream(inputWrapper);
|
||||
if (document.HasParseError()) {
|
||||
throw std::runtime_error("Failed to parse JSON config at " + configPath.string());
|
||||
throw std::runtime_error("Failed to parse " + description + " at " + jsonPath.string() +
|
||||
": " + rapidjson::GetParseError_En(document.GetParseError()));
|
||||
}
|
||||
if (!document.IsObject()) {
|
||||
throw std::runtime_error("JSON config must contain an object at the root");
|
||||
throw std::runtime_error("JSON " + description + " must contain an object at the root");
|
||||
}
|
||||
return document;
|
||||
}
|
||||
@@ -82,6 +86,23 @@ std::vector<std::filesystem::path> ExtractExtendPaths(const rapidjson::Value& do
|
||||
return paths;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveSchemaPath(const std::filesystem::path& configPath) {
|
||||
const std::filesystem::path schemaFile = "runtime_config_v2.schema.json";
|
||||
std::vector<std::filesystem::path> candidates;
|
||||
if (!configPath.empty()) {
|
||||
candidates.push_back(configPath.parent_path() / "schema" / schemaFile);
|
||||
}
|
||||
candidates.push_back(std::filesystem::current_path() / "config" / "schema" / schemaFile);
|
||||
|
||||
std::error_code ec;
|
||||
for (const auto& candidate : candidates) {
|
||||
if (!candidate.empty() && std::filesystem::exists(candidate, ec)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void ApplyDeleteDirectives(rapidjson::Value& target,
|
||||
const rapidjson::Value& overlay,
|
||||
const std::shared_ptr<ILogger>& logger,
|
||||
@@ -162,7 +183,7 @@ rapidjson::Document LoadConfigDocumentRecursive(const std::filesystem::path& con
|
||||
"configPath=" + pathKey, "Loading config document");
|
||||
}
|
||||
|
||||
rapidjson::Document document = ParseJsonDocument(normalizedPath);
|
||||
rapidjson::Document document = ParseJsonDocument(normalizedPath, "config file");
|
||||
auto extendPaths = ExtractExtendPaths(document, normalizedPath);
|
||||
if (document.HasMember(kExtendsKey)) {
|
||||
document.RemoveMember(kExtendsKey);
|
||||
@@ -209,9 +230,9 @@ std::optional<int> ReadVersionField(const rapidjson::Value& document,
|
||||
configPath.string());
|
||||
}
|
||||
|
||||
void ValidateSchemaVersion(const rapidjson::Value& document,
|
||||
const std::filesystem::path& configPath,
|
||||
const std::shared_ptr<ILogger>& logger) {
|
||||
std::optional<int> ValidateSchemaVersion(const rapidjson::Value& document,
|
||||
const std::filesystem::path& configPath,
|
||||
const std::shared_ptr<ILogger>& logger) {
|
||||
const auto schemaVersion = ReadVersionField(document, kSchemaVersionKey, configPath);
|
||||
const auto configVersion = ReadVersionField(document, kConfigVersionKey, configPath);
|
||||
if (schemaVersion && configVersion && *schemaVersion != *configVersion) {
|
||||
@@ -224,23 +245,98 @@ void ValidateSchemaVersion(const rapidjson::Value& document,
|
||||
logger->Warn("JsonConfigService::LoadFromJson: Missing schema version in " +
|
||||
configPath.string() + "; assuming version " + std::to_string(kExpectedSchemaVersion));
|
||||
}
|
||||
return;
|
||||
return std::nullopt;
|
||||
}
|
||||
if (logger) {
|
||||
logger->Trace("JsonConfigService", "ValidateSchemaVersion",
|
||||
"version=" + std::to_string(*activeVersion) +
|
||||
", configPath=" + configPath.string());
|
||||
}
|
||||
if (*activeVersion != kExpectedSchemaVersion) {
|
||||
throw std::runtime_error("Unsupported schema version " + std::to_string(*activeVersion) +
|
||||
" in " + configPath.string() +
|
||||
"; expected " + std::to_string(kExpectedSchemaVersion));
|
||||
return activeVersion;
|
||||
}
|
||||
|
||||
bool ApplyMigrations(rapidjson::Document& document,
|
||||
int fromVersion,
|
||||
int toVersion,
|
||||
const std::filesystem::path& configPath,
|
||||
const std::shared_ptr<ILogger>& logger,
|
||||
const std::shared_ptr<IProbeService>& probeService) {
|
||||
if (fromVersion == toVersion) {
|
||||
return true;
|
||||
}
|
||||
if (logger) {
|
||||
logger->Warn("JsonConfigService::ApplyMigrations: No migration path from v" +
|
||||
std::to_string(fromVersion) + " to v" + std::to_string(toVersion) +
|
||||
" for " + configPath.string());
|
||||
}
|
||||
if (probeService) {
|
||||
ProbeReport report{};
|
||||
report.severity = ProbeSeverity::Error;
|
||||
report.code = "CONFIG_MIGRATION_MISSING";
|
||||
report.jsonPath = "";
|
||||
report.message = "No migration path from v" + std::to_string(fromVersion) +
|
||||
" to v" + std::to_string(toVersion) +
|
||||
" (see config/schema/MIGRATIONS.md)";
|
||||
probeService->Report(report);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ValidateSchemaDocument(const rapidjson::Document& document,
|
||||
const std::filesystem::path& configPath,
|
||||
const std::shared_ptr<ILogger>& logger,
|
||||
const std::shared_ptr<IProbeService>& probeService) {
|
||||
const auto schemaPath = ResolveSchemaPath(configPath);
|
||||
if (schemaPath.empty()) {
|
||||
if (logger) {
|
||||
logger->Warn("JsonConfigService::ValidateSchemaDocument: Schema file not found for " +
|
||||
configPath.string());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
rapidjson::Document schemaDocument = ParseJsonDocument(schemaPath, "schema file");
|
||||
rapidjson::SchemaDocument schema(schemaDocument);
|
||||
rapidjson::SchemaValidator validator(schema);
|
||||
if (!document.Accept(validator)) {
|
||||
const std::string docPointer = validator.GetInvalidDocumentPointer().String();
|
||||
const std::string schemaPointer = validator.GetInvalidSchemaPointer().String();
|
||||
const std::string keyword = validator.GetInvalidSchemaKeyword();
|
||||
const std::string message = "JSON schema validation failed at " + docPointer +
|
||||
" (schema " + schemaPointer + ", keyword=" + keyword + ")";
|
||||
if (logger) {
|
||||
logger->Error("JsonConfigService::ValidateSchemaDocument: " + message +
|
||||
" configPath=" + configPath.string());
|
||||
}
|
||||
if (probeService) {
|
||||
ProbeReport report{};
|
||||
report.severity = ProbeSeverity::Error;
|
||||
report.code = "CONFIG_SCHEMA_INVALID";
|
||||
report.jsonPath = docPointer;
|
||||
report.message = message;
|
||||
report.details = "schemaPointer=" + schemaPointer + ", keyword=" + keyword;
|
||||
probeService->Report(report);
|
||||
}
|
||||
throw std::runtime_error("JSON schema validation failed for " + configPath.string() +
|
||||
" at " + docPointer + " (schema " + schemaPointer +
|
||||
", keyword=" + keyword + ")");
|
||||
}
|
||||
if (logger) {
|
||||
logger->Trace("JsonConfigService", "ValidateSchemaDocument",
|
||||
"schemaPath=" + schemaPath.string() +
|
||||
", configPath=" + configPath.string(),
|
||||
"Schema validation passed");
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
JsonConfigService::JsonConfigService(std::shared_ptr<ILogger> logger, const char* argv0)
|
||||
: logger_(std::move(logger)), configJson_(), config_(RuntimeConfig{}) {
|
||||
JsonConfigService::JsonConfigService(std::shared_ptr<ILogger> logger,
|
||||
const char* argv0,
|
||||
std::shared_ptr<IProbeService> probeService)
|
||||
: logger_(std::move(logger)),
|
||||
probeService_(std::move(probeService)),
|
||||
configJson_(),
|
||||
config_(RuntimeConfig{}) {
|
||||
if (logger_) {
|
||||
logger_->Trace("JsonConfigService", "JsonConfigService",
|
||||
"argv0=" + std::string(argv0 ? argv0 : ""));
|
||||
@@ -250,10 +346,14 @@ JsonConfigService::JsonConfigService(std::shared_ptr<ILogger> logger, const char
|
||||
logger_->Info("JsonConfigService initialized with default configuration");
|
||||
}
|
||||
|
||||
JsonConfigService::JsonConfigService(std::shared_ptr<ILogger> logger, const std::filesystem::path& configPath, bool dumpConfig)
|
||||
JsonConfigService::JsonConfigService(std::shared_ptr<ILogger> logger,
|
||||
const std::filesystem::path& configPath,
|
||||
bool dumpConfig,
|
||||
std::shared_ptr<IProbeService> probeService)
|
||||
: logger_(std::move(logger)),
|
||||
probeService_(std::move(probeService)),
|
||||
configJson_(),
|
||||
config_(LoadFromJson(logger_, configPath, dumpConfig, &configJson_)) {
|
||||
config_(LoadFromJson(logger_, probeService_, configPath, dumpConfig, &configJson_)) {
|
||||
if (logger_) {
|
||||
logger_->Trace("JsonConfigService", "JsonConfigService",
|
||||
"configPath=" + configPath.string() +
|
||||
@@ -262,8 +362,13 @@ JsonConfigService::JsonConfigService(std::shared_ptr<ILogger> logger, const std:
|
||||
logger_->Info("JsonConfigService initialized from config file: " + configPath.string());
|
||||
}
|
||||
|
||||
JsonConfigService::JsonConfigService(std::shared_ptr<ILogger> logger, const RuntimeConfig& config)
|
||||
: logger_(std::move(logger)), configJson_(BuildConfigJson(config, {})), config_(config) {
|
||||
JsonConfigService::JsonConfigService(std::shared_ptr<ILogger> logger,
|
||||
const RuntimeConfig& config,
|
||||
std::shared_ptr<IProbeService> probeService)
|
||||
: logger_(std::move(logger)),
|
||||
probeService_(std::move(probeService)),
|
||||
configJson_(BuildConfigJson(config, {})),
|
||||
config_(config) {
|
||||
if (logger_) {
|
||||
logger_->Trace("JsonConfigService", "JsonConfigService",
|
||||
"config.width=" + std::to_string(config.width) +
|
||||
@@ -298,6 +403,7 @@ std::filesystem::path JsonConfigService::FindScriptPath(const char* argv0) {
|
||||
}
|
||||
|
||||
RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
std::shared_ptr<IProbeService> probeService,
|
||||
const std::filesystem::path& configPath,
|
||||
bool dumpConfig,
|
||||
std::string* configJson) {
|
||||
@@ -307,7 +413,22 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
|
||||
std::unordered_set<std::string> visitedPaths;
|
||||
rapidjson::Document document = LoadConfigDocumentRecursive(configPath, logger, visitedPaths);
|
||||
ValidateSchemaVersion(document, configPath, logger);
|
||||
const auto activeVersion = ValidateSchemaVersion(document, configPath, logger);
|
||||
if (activeVersion && *activeVersion != kExpectedSchemaVersion) {
|
||||
const bool migrated = ApplyMigrations(document,
|
||||
*activeVersion,
|
||||
kExpectedSchemaVersion,
|
||||
configPath,
|
||||
logger,
|
||||
probeService);
|
||||
if (!migrated) {
|
||||
throw std::runtime_error("Unsupported schema version " + std::to_string(*activeVersion) +
|
||||
" in " + configPath.string() +
|
||||
"; expected " + std::to_string(kExpectedSchemaVersion) +
|
||||
" (see config/schema/MIGRATIONS.md)");
|
||||
}
|
||||
}
|
||||
ValidateSchemaDocument(document, configPath, logger, probeService);
|
||||
|
||||
if (dumpConfig || configJson) {
|
||||
rapidjson::StringBuffer buffer;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "../interfaces/i_config_service.hpp"
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include "../interfaces/i_probe_service.hpp"
|
||||
#include "../interfaces/config_types.hpp"
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
@@ -23,8 +24,11 @@ public:
|
||||
*
|
||||
* @param logger Logger service for logging
|
||||
* @param argv0 First command-line argument (for finding default script path)
|
||||
* @param probeService Probe service for diagnostics (optional)
|
||||
*/
|
||||
JsonConfigService(std::shared_ptr<ILogger> logger, const char* argv0);
|
||||
JsonConfigService(std::shared_ptr<ILogger> logger,
|
||||
const char* argv0,
|
||||
std::shared_ptr<IProbeService> probeService = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Construct by loading configuration from JSON.
|
||||
@@ -32,17 +36,24 @@ public:
|
||||
* @param logger Logger service for logging
|
||||
* @param configPath Path to JSON configuration file
|
||||
* @param dumpConfig Whether to print loaded config to stdout
|
||||
* @param probeService Probe service for diagnostics (optional)
|
||||
* @throws std::runtime_error if config file cannot be loaded or is invalid
|
||||
*/
|
||||
JsonConfigService(std::shared_ptr<ILogger> logger, const std::filesystem::path& configPath, bool dumpConfig);
|
||||
JsonConfigService(std::shared_ptr<ILogger> logger,
|
||||
const std::filesystem::path& configPath,
|
||||
bool dumpConfig,
|
||||
std::shared_ptr<IProbeService> probeService = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Construct with explicit configuration.
|
||||
*
|
||||
* @param logger Logger service for logging
|
||||
* @param config Runtime configuration to use
|
||||
* @param probeService Probe service for diagnostics (optional)
|
||||
*/
|
||||
JsonConfigService(std::shared_ptr<ILogger> logger, const RuntimeConfig& config);
|
||||
JsonConfigService(std::shared_ptr<ILogger> logger,
|
||||
const RuntimeConfig& config,
|
||||
std::shared_ptr<IProbeService> probeService = nullptr);
|
||||
|
||||
// IConfigService interface implementation
|
||||
uint32_t GetWindowWidth() const override {
|
||||
@@ -144,12 +155,14 @@ public:
|
||||
|
||||
private:
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
std::shared_ptr<IProbeService> probeService_;
|
||||
std::string configJson_;
|
||||
RuntimeConfig config_;
|
||||
|
||||
// Helper methods moved from main.cpp
|
||||
std::filesystem::path FindScriptPath(const char* argv0);
|
||||
static RuntimeConfig LoadFromJson(std::shared_ptr<ILogger> logger,
|
||||
std::shared_ptr<IProbeService> probeService,
|
||||
const std::filesystem::path& configPath,
|
||||
bool dumpConfig,
|
||||
std::string* configJson);
|
||||
|
||||
63
src/services/impl/probe_service.cpp
Normal file
63
src/services/impl/probe_service.cpp
Normal file
@@ -0,0 +1,63 @@
|
||||
#include "probe_service.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
ProbeService::ProbeService(std::shared_ptr<ILogger> logger)
|
||||
: logger_(std::move(logger)) {
|
||||
if (logger_) {
|
||||
logger_->Trace("ProbeService", "ProbeService", "initialized=true");
|
||||
}
|
||||
}
|
||||
|
||||
void ProbeService::Report(const ProbeReport& report) {
|
||||
reports_.push_back(report);
|
||||
LogReport(report);
|
||||
}
|
||||
|
||||
std::vector<ProbeReport> ProbeService::DrainReports() {
|
||||
std::vector<ProbeReport> drained = std::move(reports_);
|
||||
reports_.clear();
|
||||
return drained;
|
||||
}
|
||||
|
||||
void ProbeService::ClearReports() {
|
||||
reports_.clear();
|
||||
}
|
||||
|
||||
std::string ProbeService::SeverityName(ProbeSeverity severity) const {
|
||||
switch (severity) {
|
||||
case ProbeSeverity::Info:
|
||||
return "info";
|
||||
case ProbeSeverity::Warn:
|
||||
return "warn";
|
||||
case ProbeSeverity::Error:
|
||||
return "error";
|
||||
case ProbeSeverity::Fatal:
|
||||
return "fatal";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void ProbeService::LogReport(const ProbeReport& report) const {
|
||||
if (!logger_) {
|
||||
return;
|
||||
}
|
||||
const std::string summary = "severity=" + SeverityName(report.severity) +
|
||||
", code=" + report.code +
|
||||
", jsonPath=" + report.jsonPath +
|
||||
", renderPass=" + report.renderPass +
|
||||
", resourceId=" + report.resourceId;
|
||||
|
||||
if (report.severity == ProbeSeverity::Warn) {
|
||||
logger_->Warn("ProbeService::Report: " + report.message + " (" + summary + ")");
|
||||
} else if (report.severity == ProbeSeverity::Error || report.severity == ProbeSeverity::Fatal) {
|
||||
logger_->Error("ProbeService::Report: " + report.message + " (" + summary + ")");
|
||||
} else {
|
||||
logger_->Info("ProbeService::Report: " + report.message + " (" + summary + ")");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
26
src/services/impl/probe_service.hpp
Normal file
26
src/services/impl/probe_service.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "../interfaces/i_probe_service.hpp"
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
class ProbeService : public IProbeService {
|
||||
public:
|
||||
explicit ProbeService(std::shared_ptr<ILogger> logger);
|
||||
|
||||
void Report(const ProbeReport& report) override;
|
||||
std::vector<ProbeReport> DrainReports() override;
|
||||
void ClearReports() override;
|
||||
|
||||
private:
|
||||
std::string SeverityName(ProbeSeverity severity) const;
|
||||
void LogReport(const ProbeReport& report) const;
|
||||
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
std::vector<ProbeReport> reports_{};
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
174
src/services/impl/render_graph_service.cpp
Normal file
174
src/services/impl/render_graph_service.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
#include "render_graph_service.hpp"
|
||||
|
||||
#include <queue>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsErrorSeverity(ProbeSeverity severity) {
|
||||
return severity == ProbeSeverity::Error || severity == ProbeSeverity::Fatal;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RenderGraphService::RenderGraphService(std::shared_ptr<ILogger> logger,
|
||||
std::shared_ptr<IProbeService> probeService)
|
||||
: logger_(std::move(logger)),
|
||||
probeService_(std::move(probeService)) {
|
||||
if (logger_) {
|
||||
logger_->Trace("RenderGraphService", "RenderGraphService", "initialized=true");
|
||||
}
|
||||
}
|
||||
|
||||
RenderGraphBuildResult RenderGraphService::BuildGraph(const RenderGraphIR& renderGraph) {
|
||||
RenderGraphBuildResult result;
|
||||
result.success = true;
|
||||
|
||||
if (renderGraph.passes.empty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, size_t> passIndex;
|
||||
passIndex.reserve(renderGraph.passes.size());
|
||||
|
||||
for (size_t i = 0; i < renderGraph.passes.size(); ++i) {
|
||||
const auto& pass = renderGraph.passes[i];
|
||||
if (pass.id.empty()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_PASS_ID_MISSING",
|
||||
pass.jsonPath,
|
||||
"Render pass is missing an id");
|
||||
continue;
|
||||
}
|
||||
const auto [it, inserted] = passIndex.emplace(pass.id, i);
|
||||
if (!inserted) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_PASS_ID_DUPLICATE",
|
||||
pass.jsonPath,
|
||||
"Duplicate render pass id: " + pass.id);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::vector<size_t>> edges(renderGraph.passes.size());
|
||||
std::vector<size_t> indegree(renderGraph.passes.size(), 0);
|
||||
|
||||
for (size_t i = 0; i < renderGraph.passes.size(); ++i) {
|
||||
const auto& pass = renderGraph.passes[i];
|
||||
for (const auto& input : pass.inputs) {
|
||||
if (input.ref.type != RenderResourceRefType::PassOutput) {
|
||||
continue;
|
||||
}
|
||||
if (input.ref.passId.empty()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_INPUT_MISSING_PASS",
|
||||
input.jsonPath,
|
||||
"Render pass input is missing a pass reference");
|
||||
continue;
|
||||
}
|
||||
auto it = passIndex.find(input.ref.passId);
|
||||
if (it == passIndex.end()) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_INPUT_UNKNOWN_PASS",
|
||||
input.jsonPath,
|
||||
"Render pass input references unknown pass: " + input.ref.passId);
|
||||
continue;
|
||||
}
|
||||
if (it->second == i) {
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_INPUT_SELF_REFERENCE",
|
||||
input.jsonPath,
|
||||
"Render pass input references its own output");
|
||||
continue;
|
||||
}
|
||||
edges[it->second].push_back(i);
|
||||
indegree[i] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
std::queue<size_t> ready;
|
||||
for (size_t i = 0; i < indegree.size(); ++i) {
|
||||
if (indegree[i] == 0) {
|
||||
ready.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
while (!ready.empty()) {
|
||||
const size_t current = ready.front();
|
||||
ready.pop();
|
||||
result.passOrder.push_back(renderGraph.passes[current].id);
|
||||
for (size_t neighbor : edges[current]) {
|
||||
if (indegree[neighbor] > 0) {
|
||||
indegree[neighbor] -= 1;
|
||||
}
|
||||
if (indegree[neighbor] == 0) {
|
||||
ready.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.passOrder.size() != renderGraph.passes.size()) {
|
||||
std::string remaining;
|
||||
for (size_t i = 0; i < renderGraph.passes.size(); ++i) {
|
||||
if (indegree[i] > 0) {
|
||||
if (!remaining.empty()) {
|
||||
remaining += ", ";
|
||||
}
|
||||
remaining += renderGraph.passes[i].id;
|
||||
}
|
||||
}
|
||||
AddDiagnostic(result,
|
||||
ProbeSeverity::Error,
|
||||
"RG_CYCLE_DETECTED",
|
||||
"/render/passes",
|
||||
"Render graph cycle detected",
|
||||
"remainingPasses=" + remaining);
|
||||
}
|
||||
|
||||
if (logger_) {
|
||||
logger_->Trace("RenderGraphService", "BuildGraph",
|
||||
"passCount=" + std::to_string(renderGraph.passes.size()) +
|
||||
", scheduled=" + std::to_string(result.passOrder.size()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void RenderGraphService::AddDiagnostic(RenderGraphBuildResult& result,
|
||||
ProbeSeverity severity,
|
||||
const std::string& code,
|
||||
const std::string& jsonPath,
|
||||
const std::string& message,
|
||||
const std::string& details) const {
|
||||
ProbeReport report;
|
||||
report.severity = severity;
|
||||
report.code = code;
|
||||
report.jsonPath = jsonPath;
|
||||
report.message = message;
|
||||
report.details = details;
|
||||
result.diagnostics.push_back(report);
|
||||
if (IsErrorSeverity(severity)) {
|
||||
result.success = false;
|
||||
}
|
||||
if (probeService_) {
|
||||
probeService_->Report(report);
|
||||
}
|
||||
if (logger_) {
|
||||
if (severity == ProbeSeverity::Warn) {
|
||||
logger_->Warn("RenderGraphService::BuildGraph: " + message + " (" + code + ")");
|
||||
} else if (IsErrorSeverity(severity)) {
|
||||
logger_->Error("RenderGraphService::BuildGraph: " + message + " (" + code + ")");
|
||||
} else {
|
||||
logger_->Info("RenderGraphService::BuildGraph: " + message + " (" + code + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
29
src/services/impl/render_graph_service.hpp
Normal file
29
src/services/impl/render_graph_service.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "../interfaces/i_render_graph_service.hpp"
|
||||
#include "../interfaces/i_logger.hpp"
|
||||
#include "../interfaces/i_probe_service.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
class RenderGraphService : public IRenderGraphService {
|
||||
public:
|
||||
RenderGraphService(std::shared_ptr<ILogger> logger,
|
||||
std::shared_ptr<IProbeService> probeService = nullptr);
|
||||
|
||||
RenderGraphBuildResult BuildGraph(const RenderGraphIR& renderGraph) override;
|
||||
|
||||
private:
|
||||
void AddDiagnostic(RenderGraphBuildResult& result,
|
||||
ProbeSeverity severity,
|
||||
const std::string& code,
|
||||
const std::string& jsonPath,
|
||||
const std::string& message,
|
||||
const std::string& details = "") const;
|
||||
|
||||
std::shared_ptr<ILogger> logger_;
|
||||
std::shared_ptr<IProbeService> probeService_;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
107
src/services/interfaces/config_ir_types.hpp
Normal file
107
src/services/interfaces/config_ir_types.hpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
#include "probe_types.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services {
|
||||
|
||||
struct SceneEntityIR {
|
||||
std::string id;
|
||||
std::vector<std::string> componentTypes;
|
||||
std::string jsonPath;
|
||||
};
|
||||
|
||||
struct SceneIR {
|
||||
std::vector<SceneEntityIR> entities;
|
||||
};
|
||||
|
||||
struct TextureIR {
|
||||
std::string id;
|
||||
std::string uri;
|
||||
std::string jsonPath;
|
||||
};
|
||||
|
||||
struct MeshIR {
|
||||
std::string id;
|
||||
std::string uri;
|
||||
std::string jsonPath;
|
||||
};
|
||||
|
||||
struct ShaderIR {
|
||||
std::string id;
|
||||
std::string vertexPath;
|
||||
std::string fragmentPath;
|
||||
std::string jsonPath;
|
||||
};
|
||||
|
||||
struct MaterialIR {
|
||||
std::string id;
|
||||
std::string shader;
|
||||
std::unordered_map<std::string, std::string> textures;
|
||||
std::string jsonPath;
|
||||
};
|
||||
|
||||
struct ResourceIR {
|
||||
std::vector<TextureIR> textures;
|
||||
std::vector<MeshIR> meshes;
|
||||
std::vector<ShaderIR> shaders;
|
||||
std::vector<MaterialIR> materials;
|
||||
};
|
||||
|
||||
enum class RenderResourceRefType {
|
||||
None,
|
||||
PassOutput,
|
||||
Swapchain
|
||||
};
|
||||
|
||||
struct RenderResourceRef {
|
||||
RenderResourceRefType type = RenderResourceRefType::None;
|
||||
std::string passId;
|
||||
std::string outputName;
|
||||
std::string jsonPath;
|
||||
};
|
||||
|
||||
struct RenderPassInputIR {
|
||||
std::string name;
|
||||
RenderResourceRef ref;
|
||||
std::string jsonPath;
|
||||
};
|
||||
|
||||
struct RenderPassOutputIR {
|
||||
std::string name;
|
||||
bool isSwapchain = false;
|
||||
std::string format;
|
||||
std::string usage;
|
||||
std::string jsonPath;
|
||||
};
|
||||
|
||||
struct RenderPassIR {
|
||||
std::string id;
|
||||
std::string type;
|
||||
std::vector<RenderPassInputIR> inputs;
|
||||
std::vector<RenderPassOutputIR> outputs;
|
||||
std::string jsonPath;
|
||||
};
|
||||
|
||||
struct RenderGraphIR {
|
||||
std::vector<RenderPassIR> passes;
|
||||
};
|
||||
|
||||
struct RenderGraphBuildResult {
|
||||
std::vector<std::string> passOrder;
|
||||
std::vector<ProbeReport> diagnostics;
|
||||
bool success = true;
|
||||
};
|
||||
|
||||
struct ConfigCompilerResult {
|
||||
SceneIR scene;
|
||||
ResourceIR resources;
|
||||
RenderGraphIR renderGraph;
|
||||
RenderGraphBuildResult renderGraphBuild;
|
||||
std::vector<ProbeReport> diagnostics;
|
||||
bool success = true;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services
|
||||
29
src/services/interfaces/i_config_compiler_service.hpp
Normal file
29
src/services/interfaces/i_config_compiler_service.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "config_ir_types.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace sdl3cpp::services {
|
||||
|
||||
/**
|
||||
* @brief Compiles JSON configuration into validated IR structures.
|
||||
*/
|
||||
class IConfigCompilerService {
|
||||
public:
|
||||
virtual ~IConfigCompilerService() = default;
|
||||
|
||||
/**
|
||||
* @brief Compile config JSON into IR.
|
||||
*
|
||||
* @param configJson JSON payload
|
||||
* @return Compilation result (diagnostics included)
|
||||
*/
|
||||
virtual ConfigCompilerResult Compile(const std::string& configJson) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get the last compilation result.
|
||||
*/
|
||||
virtual const ConfigCompilerResult& GetLastResult() const = 0;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services
|
||||
35
src/services/interfaces/i_probe_service.hpp
Normal file
35
src/services/interfaces/i_probe_service.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "probe_types.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services {
|
||||
|
||||
/**
|
||||
* @brief Probe service interface for structured diagnostics.
|
||||
*/
|
||||
class IProbeService {
|
||||
public:
|
||||
virtual ~IProbeService() = default;
|
||||
|
||||
/**
|
||||
* @brief Record a probe report.
|
||||
*
|
||||
* @param report Report data
|
||||
*/
|
||||
virtual void Report(const ProbeReport& report) = 0;
|
||||
|
||||
/**
|
||||
* @brief Drain all queued reports.
|
||||
*
|
||||
* @return Collected reports (clears internal storage)
|
||||
*/
|
||||
virtual std::vector<ProbeReport> DrainReports() = 0;
|
||||
|
||||
/**
|
||||
* @brief Clear all queued reports.
|
||||
*/
|
||||
virtual void ClearReports() = 0;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services
|
||||
23
src/services/interfaces/i_render_graph_service.hpp
Normal file
23
src/services/interfaces/i_render_graph_service.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "config_ir_types.hpp"
|
||||
|
||||
namespace sdl3cpp::services {
|
||||
|
||||
/**
|
||||
* @brief Builds and validates render graphs.
|
||||
*/
|
||||
class IRenderGraphService {
|
||||
public:
|
||||
virtual ~IRenderGraphService() = default;
|
||||
|
||||
/**
|
||||
* @brief Build a render graph schedule.
|
||||
*
|
||||
* @param renderGraph Render graph IR
|
||||
* @return Schedule and diagnostics
|
||||
*/
|
||||
virtual RenderGraphBuildResult BuildGraph(const RenderGraphIR& renderGraph) = 0;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services
|
||||
30
src/services/interfaces/probe_types.hpp
Normal file
30
src/services/interfaces/probe_types.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace sdl3cpp::services {
|
||||
|
||||
/**
|
||||
* @brief Severity levels for probe reports.
|
||||
*/
|
||||
enum class ProbeSeverity {
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Fatal
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Structured probe report for diagnostics and policy handling.
|
||||
*/
|
||||
struct ProbeReport {
|
||||
ProbeSeverity severity = ProbeSeverity::Info;
|
||||
std::string code;
|
||||
std::string jsonPath;
|
||||
std::string renderPass;
|
||||
std::string resourceId;
|
||||
std::string message;
|
||||
std::string details;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services
|
||||
Reference in New Issue
Block a user