Files
SDL3CPlusPlus/src/services/impl/config_compiler_service.cpp
johndoe6345789 af418dcdd2 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.
2026-01-08 18:56:31 +00:00

580 lines
27 KiB
C++

#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