feat(render-graph): Implement Lua-based render graph system

- Added render graph configuration to JSON files and runtime settings.
- Introduced RenderGraphScriptService to load and manage render graph definitions from Lua.
- Updated GraphicsService to handle render graph definitions.
- Enhanced cube_logic.lua with a sample render graph function.
- Modified various services and interfaces to support render graph functionality.
- Improved logging for render graph operations and configurations.
This commit is contained in:
2026-01-06 00:14:00 +00:00
parent 84cd9e4f46
commit 01f1e94994
19 changed files with 795 additions and 13 deletions

View File

@@ -65,6 +65,7 @@
"fog_density": 0.003,
"fog_color": [0.05, 0.05, 0.08],
"gamma": 2.2,
"exposure": 1.0,
"enable_tone_mapping": true,
"enable_shadows": true,
"enable_ssgi": true,
@@ -72,6 +73,10 @@
"pbr_roughness": 0.3,
"pbr_metallic": 0.1
},
"render_graph": {
"enabled": true,
"function": "get_render_graph"
},
"gui_opacity": 1.0,
"config_file": "config/seed_runtime.json"
}

View File

@@ -10,6 +10,10 @@
"scripts_directory": "scripts",
"project_root": "../",
"shaders_directory": "shaders",
"render_graph": {
"enabled": false,
"function": "get_render_graph"
},
"vita_specific": {
"analog_stick_sensitivity": 1.2,
"gyroscope_enabled": true,
@@ -72,4 +76,4 @@
"shadows_enabled": true,
"particle_effects": true
}
}
}

View File

@@ -9,8 +9,6 @@ local cube_mesh_info = {
local cube_vertices = {}
local cube_indices = {}
local cube_indices_double_sided = {}
local cube_indices_active = {}
local use_double_sided_indices = false
local function build_double_sided_indices(indices)
local doubled = {}
@@ -51,8 +49,6 @@ local function load_cube_mesh()
cube_vertices = mesh.vertices
cube_indices = mesh.indices
cube_indices_double_sided = build_double_sided_indices(cube_indices)
use_double_sided_indices = #cube_indices_double_sided > 0
cube_indices_active = use_double_sided_indices and cube_indices_double_sided or cube_indices
cube_mesh_info.loaded = true
cube_mesh_info.vertex_count = #mesh.vertices
cube_mesh_info.index_count = #mesh.indices
@@ -222,8 +218,8 @@ end
if cube_mesh_info.loaded then
log_debug("Loaded cube mesh from %s (%d vertices, %d indices)",
cube_mesh_info.path, cube_mesh_info.vertex_count, cube_mesh_info.index_count)
if use_double_sided_indices then
log_debug("Using double-sided cube indices (%d -> %d)",
if #cube_indices_double_sided > 0 then
log_debug("Built double-sided cube indices (%d -> %d)",
cube_mesh_info.index_count, #cube_indices_double_sided)
end
end
@@ -339,7 +335,7 @@ local camera = {
}
local controls = {
move_speed = 4.0,
move_speed = 16.0,
fly_speed = 3.0,
jump_speed = 5.5,
gravity = -12.0,
@@ -640,7 +636,7 @@ local function create_static_cube(position, scale, color, shader_key)
return {
vertices = vertices,
indices = cube_indices_active,
indices = cube_indices,
compute_model_matrix = compute_model_matrix,
shader_key = resolved_shader,
}
@@ -657,7 +653,7 @@ local function create_spinning_cube()
return {
vertices = cube_vertices,
indices = cube_indices_active,
indices = (#cube_indices_double_sided > 0) and cube_indices_double_sided or cube_indices,
compute_model_matrix = compute_model_matrix,
shader_key = "default",
}
@@ -845,6 +841,114 @@ function get_shader_paths()
return shader_variants
end
local function resolve_number(value, fallback)
if type(value) == "number" then
return value
end
return fallback
end
function get_render_graph()
local atmospherics = {}
if type(config) == "table" and type(config.atmospherics) == "table" then
atmospherics = config.atmospherics
end
local exposure = resolve_number(atmospherics.exposure, 1.0)
local gamma = resolve_number(atmospherics.gamma, 2.2)
local fog_density = resolve_number(atmospherics.fog_density, 0.003)
return {
resources = {
scene_hdr = {type = "color", format = "rgba16f", size = "swapchain"},
depth = {type = "depth", format = "d32", size = "swapchain"},
normal_rough = {type = "color", format = "a2b10g10r10", size = "swapchain"},
motion = {type = "color", format = "rg16f", size = "swapchain"},
shadow_csm = {type = "depth_array", format = "d32", size = {2048, 2048}, layers = 4},
ao = {type = "color", format = "r8", size = "half"},
ssr = {type = "color", format = "rgba16f", size = "swapchain"},
volumetric = {type = "color", format = "rgba16f", size = "half"},
taa_history = {type = "color", format = "rgba16f", size = "swapchain"},
bloom = {type = "color", format = "rgba16f", size = "half", mips = 5},
},
passes = {
{
name = "shadow",
kind = "shadow_csm",
output = "shadow_csm",
settings = {cascades = 4, bias = 0.002, normal_bias = 0.02, pcf = 5},
},
{
name = "gbuffer",
kind = "gbuffer",
shader = "pbr",
outputs = {
color = "scene_hdr",
depth = "depth",
normal = "normal_rough",
motion = "motion",
},
},
{
name = "ssao",
kind = "fullscreen",
shader = "ssao",
inputs = {depth = "depth", normal = "normal_rough"},
output = "ao",
settings = {radius = 0.5, power = 1.3},
},
{
name = "ssr",
kind = "fullscreen",
shader = "ssr",
inputs = {scene = "scene_hdr", depth = "depth", normal = "normal_rough"},
output = "ssr",
settings = {max_steps = 64, thickness = 0.1, roughness_fallback = 0.7},
},
{
name = "volumetric",
kind = "fullscreen",
shader = "volumetric",
inputs = {scene = "scene_hdr", depth = "depth"},
output = "volumetric",
settings = {density = 0.02},
},
{
name = "height_fog",
kind = "fullscreen",
shader = "height_fog",
inputs = {scene = "scene_hdr", depth = "depth"},
output = "scene_hdr",
settings = {height = 2.5, falloff = fog_density * 80.0},
},
{
name = "taa",
kind = "taa",
inputs = {scene = "scene_hdr", history = "taa_history", motion = "motion"},
output = "scene_hdr",
settings = {feedback = 0.9, sharpen = 0.2},
},
{
name = "bloom",
kind = "bloom",
input = "scene_hdr",
output = "scene_hdr",
settings = {threshold = 1.0, intensity = 0.7},
},
{
name = "tonemap",
kind = "fullscreen",
shader = "tonemap",
input = "scene_hdr",
output = "swapchain",
settings = {exposure = exposure, gamma = gamma, curve = "aces", grade = "warm"},
},
},
}
end
function get_view_projection(aspect)
local now = os.clock()
local dt = 0.0

View File

@@ -22,6 +22,7 @@
#include "services/impl/script_engine_service.hpp"
#include "services/impl/scene_script_service.hpp"
#include "services/impl/shader_script_service.hpp"
#include "services/impl/render_graph_script_service.hpp"
#include "services/impl/gui_script_service.hpp"
#include "services/impl/audio_command_service.hpp"
#include "services/impl/physics_bridge_service.hpp"
@@ -276,6 +277,10 @@ void ServiceBasedApp::RegisterServices() {
registry_.RegisterService<services::IShaderScriptService, services::impl::ShaderScriptService>(
registry_.GetService<services::IScriptEngineService>(),
registry_.GetService<services::ILogger>());
registry_.RegisterService<services::IRenderGraphScriptService, services::impl::RenderGraphScriptService>(
registry_.GetService<services::IScriptEngineService>(),
registry_.GetService<services::IConfigService>(),
registry_.GetService<services::ILogger>());
registry_.RegisterService<services::IGuiScriptService, services::impl::GuiScriptService>(
registry_.GetService<services::IScriptEngineService>(),
registry_.GetService<services::ILogger>());
@@ -356,6 +361,7 @@ void ServiceBasedApp::RegisterServices() {
registry_.GetService<services::IGraphicsService>(),
registry_.GetService<services::ISceneScriptService>(),
registry_.GetService<services::IShaderScriptService>(),
registry_.GetService<services::IRenderGraphScriptService>(),
registry_.GetService<services::IGuiScriptService>(),
registry_.GetService<services::IGuiService>(),
registry_.GetService<services::ISceneService>());

View File

@@ -106,6 +106,19 @@ void GraphicsService::LoadShaders(const std::unordered_map<std::string, ShaderPa
}
}
void GraphicsService::SetRenderGraphDefinition(const RenderGraphDefinition& definition) {
logger_->Trace("GraphicsService", "SetRenderGraphDefinition",
"resources=" + std::to_string(definition.resources.size()) +
", passes=" + std::to_string(definition.passes.size()));
renderGraphDefinition_ = definition;
}
const RenderGraphDefinition& GraphicsService::GetRenderGraphDefinition() const {
logger_->Trace("GraphicsService", "GetRenderGraphDefinition");
return renderGraphDefinition_;
}
void GraphicsService::UploadVertexData(const std::vector<core::Vertex>& vertices) {
logger_->Trace("GraphicsService", "UploadVertexData",
"vertices.size=" + std::to_string(vertices.size()));

View File

@@ -34,6 +34,8 @@ public:
void InitializeSwapchain() override;
void RecreateSwapchain() override;
void LoadShaders(const std::unordered_map<std::string, ShaderPaths>& shaders) override;
void SetRenderGraphDefinition(const RenderGraphDefinition& definition) override;
const RenderGraphDefinition& GetRenderGraphDefinition() const override;
void UploadVertexData(const std::vector<core::Vertex>& vertices) override;
void UploadIndexData(const std::vector<uint16_t>& indices) override;
bool BeginFrame() override;
@@ -56,8 +58,9 @@ private:
std::unordered_map<std::string, GraphicsPipelineHandle> pipelines_;
GraphicsBufferHandle vertexBuffer_;
GraphicsBufferHandle indexBuffer_;
RenderGraphDefinition renderGraphDefinition_{};
// Other state
bool initialized_ = false;
};
} // namespace sdl3cpp::services::impl
} // namespace sdl3cpp::services::impl

View File

@@ -353,6 +353,7 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
readFloat("fog_density", config.atmospherics.fogDensity);
readFloatArray3("fog_color", config.atmospherics.fogColor);
readFloat("gamma", config.atmospherics.gamma);
readFloat("exposure", config.atmospherics.exposure);
readBool("enable_tone_mapping", config.atmospherics.enableToneMapping);
readBool("enable_shadows", config.atmospherics.enableShadows);
readBool("enable_ssgi", config.atmospherics.enableSSGI);
@@ -361,6 +362,29 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
readFloat("pbr_metallic", config.atmospherics.pbrMetallic);
}
if (document.HasMember("render_graph")) {
const auto& renderGraphValue = document["render_graph"];
if (!renderGraphValue.IsObject()) {
throw std::runtime_error("JSON member 'render_graph' must be an object");
}
if (renderGraphValue.HasMember("enabled")) {
const auto& value = renderGraphValue["enabled"];
if (!value.IsBool()) {
throw std::runtime_error("JSON member 'render_graph.enabled' must be a boolean");
}
config.renderGraph.enabled = value.GetBool();
}
if (renderGraphValue.HasMember("function")) {
const auto& value = renderGraphValue["function"];
if (!value.IsString()) {
throw std::runtime_error("JSON member 'render_graph.function' must be a string");
}
config.renderGraph.functionName = value.GetString();
}
}
if (document.HasMember("gui_opacity")) {
const auto& value = document["gui_opacity"];
if (!value.IsNumber()) {
@@ -410,6 +434,13 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config,
allocator);
document.AddMember("mouse_grab", mouseGrabObject, allocator);
rapidjson::Value renderGraphObject(rapidjson::kObjectType);
renderGraphObject.AddMember("enabled", config.renderGraph.enabled, allocator);
renderGraphObject.AddMember("function",
rapidjson::Value(config.renderGraph.functionName.c_str(), allocator),
allocator);
document.AddMember("render_graph", renderGraphObject, allocator);
rapidjson::Value bindingsObject(rapidjson::kObjectType);
auto addBindingMember = [&](const char* name, const std::string& value) {
rapidjson::Value nameValue(name, allocator);

View File

@@ -88,6 +88,12 @@ public:
}
return config_.mouseGrab;
}
const RenderGraphConfig& GetRenderGraphConfig() const override {
if (logger_) {
logger_->Trace("JsonConfigService", "GetRenderGraphConfig");
}
return config_.renderGraph;
}
const std::string& GetConfigJson() const override {
if (logger_) {
logger_->Trace("JsonConfigService", "GetConfigJson");

View File

@@ -204,7 +204,7 @@ void RenderCommandService::RecordCommands(uint32_t imageIndex,
pushConstants.fogEnd = 100.0f;
pushConstants.fogColor = config.atmospherics.fogColor;
pushConstants.gamma = config.atmospherics.gamma;
pushConstants.exposure = 1.0f;
pushConstants.exposure = config.atmospherics.exposure;
pushConstants.enableShadows = config.atmospherics.enableShadows ? 1 : 0;
pushConstants.enableFog = 1; // Enable fog for PBR
}

View File

@@ -6,6 +6,7 @@ RenderCoordinatorService::RenderCoordinatorService(std::shared_ptr<ILogger> logg
std::shared_ptr<IGraphicsService> graphicsService,
std::shared_ptr<ISceneScriptService> sceneScriptService,
std::shared_ptr<IShaderScriptService> shaderScriptService,
std::shared_ptr<IRenderGraphScriptService> renderGraphScriptService,
std::shared_ptr<IGuiScriptService> guiScriptService,
std::shared_ptr<IGuiService> guiService,
std::shared_ptr<ISceneService> sceneService)
@@ -13,6 +14,7 @@ RenderCoordinatorService::RenderCoordinatorService(std::shared_ptr<ILogger> logg
graphicsService_(std::move(graphicsService)),
sceneScriptService_(std::move(sceneScriptService)),
shaderScriptService_(std::move(shaderScriptService)),
renderGraphScriptService_(std::move(renderGraphScriptService)),
guiScriptService_(std::move(guiScriptService)),
guiService_(std::move(guiService)),
sceneService_(std::move(sceneService)) {
@@ -21,6 +23,7 @@ RenderCoordinatorService::RenderCoordinatorService(std::shared_ptr<ILogger> logg
"graphicsService=" + std::string(graphicsService_ ? "set" : "null") +
", sceneScriptService=" + std::string(sceneScriptService_ ? "set" : "null") +
", shaderScriptService=" + std::string(shaderScriptService_ ? "set" : "null") +
", renderGraphScriptService=" + std::string(renderGraphScriptService_ ? "set" : "null") +
", guiScriptService=" + std::string(guiScriptService_ ? "set" : "null") +
", guiService=" + std::string(guiService_ ? "set" : "null") +
", sceneService=" + std::string(sceneService_ ? "set" : "null"),
@@ -41,6 +44,21 @@ void RenderCoordinatorService::RenderFrame(float time) {
return;
}
if (!renderGraphInitialized_) {
renderGraphInitialized_ = true;
if (renderGraphScriptService_ && renderGraphScriptService_->IsEnabled()) {
if (logger_) {
logger_->Trace("RenderCoordinatorService", "RenderFrame",
"Loading render graph from Lua");
}
auto graph = renderGraphScriptService_->LoadRenderGraph();
graphicsService_->SetRenderGraphDefinition(graph);
} else if (logger_) {
logger_->Trace("RenderCoordinatorService", "RenderFrame",
"Render graph disabled or unavailable");
}
}
if (!shadersLoaded_) {
if (!shaderScriptService_) {
if (logger_) {

View File

@@ -5,6 +5,7 @@
#include "../interfaces/i_gui_script_service.hpp"
#include "../interfaces/i_gui_service.hpp"
#include "../interfaces/i_logger.hpp"
#include "../interfaces/i_render_graph_script_service.hpp"
#include "../interfaces/i_scene_script_service.hpp"
#include "../interfaces/i_scene_service.hpp"
#include "../interfaces/i_shader_script_service.hpp"
@@ -18,6 +19,7 @@ public:
std::shared_ptr<IGraphicsService> graphicsService,
std::shared_ptr<ISceneScriptService> sceneScriptService,
std::shared_ptr<IShaderScriptService> shaderScriptService,
std::shared_ptr<IRenderGraphScriptService> renderGraphScriptService,
std::shared_ptr<IGuiScriptService> guiScriptService,
std::shared_ptr<IGuiService> guiService,
std::shared_ptr<ISceneService> sceneService);
@@ -30,6 +32,7 @@ private:
std::shared_ptr<IGraphicsService> graphicsService_;
std::shared_ptr<ISceneScriptService> sceneScriptService_;
std::shared_ptr<IShaderScriptService> shaderScriptService_;
std::shared_ptr<IRenderGraphScriptService> renderGraphScriptService_;
std::shared_ptr<IGuiScriptService> guiScriptService_;
std::shared_ptr<IGuiService> guiService_;
std::shared_ptr<ISceneService> sceneService_;
@@ -37,6 +40,7 @@ private:
size_t lastIndexCount_ = 0;
bool shadersLoaded_ = false;
bool geometryUploaded_ = false;
bool renderGraphInitialized_ = false;
};
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,417 @@
#include "render_graph_script_service.hpp"
#include "lua_helpers.hpp"
#include "services/interfaces/i_logger.hpp"
#include <lua.hpp>
#include <stdexcept>
#include <string>
#include <utility>
namespace sdl3cpp::services::impl {
RenderGraphScriptService::RenderGraphScriptService(std::shared_ptr<IScriptEngineService> engineService,
std::shared_ptr<IConfigService> configService,
std::shared_ptr<ILogger> logger)
: engineService_(std::move(engineService)),
configService_(std::move(configService)),
logger_(std::move(logger)) {
if (logger_) {
logger_->Trace("RenderGraphScriptService", "RenderGraphScriptService",
"engineService=" + std::string(engineService_ ? "set" : "null") +
", configService=" + std::string(configService_ ? "set" : "null"));
}
}
bool RenderGraphScriptService::IsEnabled() const {
if (!configService_) {
return false;
}
return configService_->GetRenderGraphConfig().enabled;
}
bool RenderGraphScriptService::HasRenderGraphFunction() const {
if (!IsEnabled()) {
return false;
}
lua_State* L = GetLuaState();
const auto& config = GetRenderGraphConfig();
lua_getglobal(L, config.functionName.c_str());
bool isFunction = lua_isfunction(L, -1);
lua_pop(L, 1);
return isFunction;
}
RenderGraphDefinition RenderGraphScriptService::LoadRenderGraph() {
if (logger_) {
logger_->Trace("RenderGraphScriptService", "LoadRenderGraph");
}
if (!IsEnabled()) {
if (logger_) {
logger_->Trace("RenderGraphScriptService", "LoadRenderGraph",
"renderGraphEnabled=false");
}
return {};
}
if (graphLoaded_) {
return cachedGraph_;
}
lua_State* L = GetLuaState();
const auto& config = GetRenderGraphConfig();
lua_getglobal(L, config.functionName.c_str());
if (!lua_isfunction(L, -1)) {
lua_pop(L, 1);
if (logger_) {
logger_->Error("Lua render graph function '" + config.functionName + "' is missing");
}
throw std::runtime_error("Lua render graph function '" + config.functionName + "' is missing");
}
if (lua_pcall(L, 0, 1, 0) != LUA_OK) {
std::string message = lua::GetLuaError(L);
lua_pop(L, 1);
if (logger_) {
logger_->Error("Lua render graph failed: " + message);
}
throw std::runtime_error("Lua render graph failed: " + message);
}
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
if (logger_) {
logger_->Error("Render graph function did not return a table");
}
throw std::runtime_error("Render graph function did not return a table");
}
cachedGraph_ = ParseRenderGraph(L, lua_gettop(L));
graphLoaded_ = true;
lua_pop(L, 1);
if (logger_) {
logger_->Info("Loaded render graph with resources=" +
std::to_string(cachedGraph_.resources.size()) +
", passes=" + std::to_string(cachedGraph_.passes.size()));
}
return cachedGraph_;
}
lua_State* RenderGraphScriptService::GetLuaState() const {
if (logger_) {
logger_->Trace("RenderGraphScriptService", "GetLuaState");
}
if (!engineService_) {
throw std::runtime_error("Render graph script service is missing script engine service");
}
lua_State* state = engineService_->GetLuaState();
if (!state) {
throw std::runtime_error("Lua state is not initialized");
}
return state;
}
const RenderGraphConfig& RenderGraphScriptService::GetRenderGraphConfig() const {
if (!configService_) {
throw std::runtime_error("Render graph script service is missing config service");
}
return configService_->GetRenderGraphConfig();
}
RenderGraphDefinition RenderGraphScriptService::ParseRenderGraph(lua_State* L, int index) const {
RenderGraphDefinition graph;
int graphIndex = lua_absindex(L, index);
lua_getfield(L, graphIndex, "resources");
if (lua_istable(L, -1)) {
int resourcesIndex = lua_absindex(L, -1);
lua_pushnil(L);
while (lua_next(L, resourcesIndex) != 0) {
if (lua_isstring(L, -2) && lua_istable(L, -1)) {
std::string name = lua_tostring(L, -2);
graph.resources.push_back(ParseResource(L, lua_gettop(L), name));
} else if (logger_) {
logger_->Warn("RenderGraphScriptService: Skipping invalid resource entry");
}
lua_pop(L, 1);
}
} else if (!lua_isnil(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Render graph 'resources' must be a table when provided");
}
lua_pop(L, 1);
lua_getfield(L, graphIndex, "passes");
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Render graph 'passes' must be a table");
}
int passesIndex = lua_absindex(L, -1);
size_t count = lua_rawlen(L, passesIndex);
graph.passes.reserve(count);
for (size_t i = 1; i <= count; ++i) {
lua_rawgeti(L, passesIndex, static_cast<int>(i));
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Render graph pass at index " +
std::to_string(i) + " must be a table");
}
graph.passes.push_back(ParsePass(L, lua_gettop(L), i));
lua_pop(L, 1);
}
lua_pop(L, 1);
if (graph.passes.empty()) {
throw std::runtime_error("Render graph must define at least one pass");
}
return graph;
}
RenderGraphResource RenderGraphScriptService::ParseResource(lua_State* L, int index,
const std::string& name) const {
RenderGraphResource resource;
resource.name = name;
int resourceIndex = lua_absindex(L, index);
auto readRequiredString = [&](const char* field, std::string& target) {
lua_getfield(L, resourceIndex, field);
if (!lua_isstring(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Render graph resource '" + name +
"' field '" + field + "' must be a string");
}
target = lua_tostring(L, -1);
lua_pop(L, 1);
};
auto readOptionalNumber = [&](const char* field, uint32_t& target) {
lua_getfield(L, resourceIndex, field);
if (lua_isnumber(L, -1)) {
double value = lua_tonumber(L, -1);
target = value < 0.0 ? 0u : static_cast<uint32_t>(value);
} else if (!lua_isnil(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Render graph resource '" + name +
"' field '" + field + "' must be a number");
}
lua_pop(L, 1);
};
readRequiredString("type", resource.type);
readRequiredString("format", resource.format);
resource.size = "swapchain";
lua_getfield(L, resourceIndex, "size");
if (lua_isstring(L, -1)) {
resource.size = lua_tostring(L, -1);
} else if (lua_istable(L, -1)) {
int sizeIndex = lua_absindex(L, -1);
bool parsed = false;
size_t sizeLen = lua_rawlen(L, sizeIndex);
if (sizeLen >= 2) {
lua_rawgeti(L, sizeIndex, 1);
lua_rawgeti(L, sizeIndex, 2);
if (lua_isnumber(L, -2) && lua_isnumber(L, -1)) {
resource.explicitSize[0] = static_cast<uint32_t>(lua_tonumber(L, -2));
resource.explicitSize[1] = static_cast<uint32_t>(lua_tonumber(L, -1));
resource.hasExplicitSize = true;
parsed = true;
}
lua_pop(L, 2);
}
if (!parsed) {
lua_getfield(L, sizeIndex, "width");
lua_getfield(L, sizeIndex, "height");
if (lua_isnumber(L, -2) && lua_isnumber(L, -1)) {
resource.explicitSize[0] = static_cast<uint32_t>(lua_tonumber(L, -2));
resource.explicitSize[1] = static_cast<uint32_t>(lua_tonumber(L, -1));
resource.hasExplicitSize = true;
parsed = true;
}
lua_pop(L, 2);
}
if (!parsed) {
lua_pop(L, 1);
throw std::runtime_error("Render graph resource '" + name +
"' size must be a string or {width,height}");
}
} else if (!lua_isnil(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Render graph resource '" + name +
"' size must be a string or table");
}
lua_pop(L, 1);
readOptionalNumber("layers", resource.layers);
readOptionalNumber("mips", resource.mips);
return resource;
}
RenderGraphPass RenderGraphScriptService::ParsePass(lua_State* L, int index, size_t passIndex) const {
RenderGraphPass pass;
int passTableIndex = lua_absindex(L, index);
lua_getfield(L, passTableIndex, "name");
if (lua_isstring(L, -1)) {
pass.name = lua_tostring(L, -1);
} else {
pass.name = "pass_" + std::to_string(passIndex);
}
lua_pop(L, 1);
lua_getfield(L, passTableIndex, "kind");
if (!lua_isstring(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Render graph pass '" + pass.name + "' missing 'kind' string");
}
pass.kind = lua_tostring(L, -1);
lua_pop(L, 1);
lua_getfield(L, passTableIndex, "shader");
if (lua_isstring(L, -1)) {
pass.shader = lua_tostring(L, -1);
} else if (!lua_isnil(L, -1) && logger_) {
logger_->Warn("RenderGraphScriptService: pass '" + pass.name + "' shader must be a string");
}
lua_pop(L, 1);
lua_getfield(L, passTableIndex, "output");
if (lua_isstring(L, -1)) {
pass.output = lua_tostring(L, -1);
} else if (!lua_isnil(L, -1) && logger_) {
logger_->Warn("RenderGraphScriptService: pass '" + pass.name + "' output must be a string");
}
lua_pop(L, 1);
std::string inputValue;
lua_getfield(L, passTableIndex, "input");
if (lua_isstring(L, -1)) {
inputValue = lua_tostring(L, -1);
} else if (!lua_isnil(L, -1) && logger_) {
logger_->Warn("RenderGraphScriptService: pass '" + pass.name + "' input must be a string");
}
lua_pop(L, 1);
lua_getfield(L, passTableIndex, "inputs");
if (lua_istable(L, -1)) {
pass.inputs = ParseStringMap(L, lua_gettop(L));
} else if (!lua_isnil(L, -1) && logger_) {
logger_->Warn("RenderGraphScriptService: pass '" + pass.name + "' inputs must be a table");
}
lua_pop(L, 1);
if (!inputValue.empty() && pass.inputs.find("input") == pass.inputs.end()) {
pass.inputs.emplace("input", std::move(inputValue));
}
lua_getfield(L, passTableIndex, "outputs");
if (lua_istable(L, -1)) {
pass.outputs = ParseStringMap(L, lua_gettop(L));
} else if (!lua_isnil(L, -1) && logger_) {
logger_->Warn("RenderGraphScriptService: pass '" + pass.name + "' outputs must be a table");
}
lua_pop(L, 1);
lua_getfield(L, passTableIndex, "settings");
if (lua_istable(L, -1)) {
pass.settings = ParseSettingsMap(L, lua_gettop(L));
} else if (!lua_isnil(L, -1) && logger_) {
logger_->Warn("RenderGraphScriptService: pass '" + pass.name + "' settings must be a table");
}
lua_pop(L, 1);
return pass;
}
std::unordered_map<std::string, std::string> RenderGraphScriptService::ParseStringMap(lua_State* L,
int index) const {
std::unordered_map<std::string, std::string> result;
int mapIndex = lua_absindex(L, index);
lua_pushnil(L);
while (lua_next(L, mapIndex) != 0) {
if (lua_isstring(L, -2) && lua_isstring(L, -1)) {
std::string key = lua_tostring(L, -2);
std::string value = lua_tostring(L, -1);
result.emplace(std::move(key), std::move(value));
} else if (logger_) {
logger_->Warn("RenderGraphScriptService: String map entry must be string->string");
}
lua_pop(L, 1);
}
return result;
}
std::unordered_map<std::string, RenderGraphValue> RenderGraphScriptService::ParseSettingsMap(lua_State* L,
int index) const {
std::unordered_map<std::string, RenderGraphValue> result;
int mapIndex = lua_absindex(L, index);
lua_pushnil(L);
while (lua_next(L, mapIndex) != 0) {
if (!lua_isstring(L, -2)) {
lua_pop(L, 1);
continue;
}
std::string key = lua_tostring(L, -2);
RenderGraphValue value;
if (TryParseValue(L, lua_gettop(L), value)) {
result.emplace(std::move(key), std::move(value));
} else if (logger_) {
logger_->Warn("RenderGraphScriptService: Unsupported settings value for key '" + key + "'");
}
lua_pop(L, 1);
}
return result;
}
bool RenderGraphScriptService::TryParseValue(lua_State* L, int index, RenderGraphValue& outValue) const {
int absIndex = lua_absindex(L, index);
if (lua_isnumber(L, absIndex)) {
outValue.type = RenderGraphValue::Type::Number;
outValue.number = lua_tonumber(L, absIndex);
return true;
}
if (lua_isboolean(L, absIndex)) {
outValue.type = RenderGraphValue::Type::Boolean;
outValue.boolean = lua_toboolean(L, absIndex) != 0;
return true;
}
if (lua_isstring(L, absIndex)) {
outValue.type = RenderGraphValue::Type::String;
outValue.string = lua_tostring(L, absIndex);
return true;
}
if (lua_istable(L, absIndex)) {
size_t len = lua_rawlen(L, absIndex);
if (len == 0) {
return false;
}
outValue.type = RenderGraphValue::Type::Array;
outValue.array.clear();
outValue.array.reserve(len);
for (size_t i = 1; i <= len; ++i) {
lua_rawgeti(L, absIndex, static_cast<int>(i));
if (!lua_isnumber(L, -1)) {
lua_pop(L, 1);
outValue.array.clear();
return false;
}
outValue.array.push_back(lua_tonumber(L, -1));
lua_pop(L, 1);
}
return true;
}
return false;
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,43 @@
#pragma once
#include "../interfaces/i_render_graph_script_service.hpp"
#include "../interfaces/i_config_service.hpp"
#include "../interfaces/i_logger.hpp"
#include "../interfaces/i_script_engine_service.hpp"
#include <memory>
struct lua_State;
namespace sdl3cpp::services::impl {
/**
* @brief Loads render graph definitions from the shared Lua runtime.
*/
class RenderGraphScriptService : public IRenderGraphScriptService {
public:
RenderGraphScriptService(std::shared_ptr<IScriptEngineService> engineService,
std::shared_ptr<IConfigService> configService,
std::shared_ptr<ILogger> logger);
bool IsEnabled() const override;
bool HasRenderGraphFunction() const override;
RenderGraphDefinition LoadRenderGraph() override;
private:
lua_State* GetLuaState() const;
const RenderGraphConfig& GetRenderGraphConfig() const;
RenderGraphDefinition ParseRenderGraph(lua_State* L, int index) const;
RenderGraphResource ParseResource(lua_State* L, int index, const std::string& name) const;
RenderGraphPass ParsePass(lua_State* L, int index, size_t passIndex) const;
bool TryParseValue(lua_State* L, int index, RenderGraphValue& outValue) const;
std::unordered_map<std::string, std::string> ParseStringMap(lua_State* L, int index) const;
std::unordered_map<std::string, RenderGraphValue> ParseSettingsMap(lua_State* L, int index) const;
std::shared_ptr<IScriptEngineService> engineService_;
std::shared_ptr<IConfigService> configService_;
std::shared_ptr<ILogger> logger_;
bool graphLoaded_ = false;
RenderGraphDefinition cachedGraph_{};
};
} // namespace sdl3cpp::services::impl

View File

@@ -71,6 +71,7 @@ struct AtmosphericsConfig {
float fogDensity = 0.003f;
std::array<float, 3> fogColor = {0.05f, 0.05f, 0.08f};
float gamma = 2.2f;
float exposure = 1.0f;
bool enableToneMapping = true;
bool enableShadows = true;
bool enableSSGI = true;
@@ -79,6 +80,14 @@ struct AtmosphericsConfig {
float pbrMetallic = 0.1f;
};
/**
* @brief Lua-defined render graph configuration.
*/
struct RenderGraphConfig {
bool enabled = false;
std::string functionName = "get_render_graph";
};
/**
* @brief Runtime configuration values used across services.
*/
@@ -91,6 +100,7 @@ struct RuntimeConfig {
MouseGrabConfig mouseGrab{};
InputBindings inputBindings{};
AtmosphericsConfig atmospherics{};
RenderGraphConfig renderGraph{};
float guiOpacity = 1.0f;
};

View File

@@ -1,9 +1,10 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
#include <array>
namespace sdl3cpp::services {
@@ -45,4 +46,57 @@ struct RenderCommand {
std::array<float, 16> modelMatrix;
};
/**
* @brief Render graph value used for Lua-defined settings.
*/
struct RenderGraphValue {
enum class Type {
Number,
Boolean,
String,
Array
};
Type type = Type::Number;
double number = 0.0;
bool boolean = false;
std::string string;
std::vector<double> array;
};
/**
* @brief Render graph resource definition loaded from Lua.
*/
struct RenderGraphResource {
std::string name;
std::string type;
std::string format;
std::string size;
std::array<uint32_t, 2> explicitSize{};
bool hasExplicitSize = false;
uint32_t layers = 1;
uint32_t mips = 1;
};
/**
* @brief Render graph pass definition loaded from Lua.
*/
struct RenderGraphPass {
std::string name;
std::string kind;
std::string shader;
std::string output;
std::unordered_map<std::string, std::string> inputs;
std::unordered_map<std::string, std::string> outputs;
std::unordered_map<std::string, RenderGraphValue> settings;
};
/**
* @brief Render graph definition loaded from Lua.
*/
struct RenderGraphDefinition {
std::vector<RenderGraphResource> resources;
std::vector<RenderGraphPass> passes;
};
} // namespace sdl3cpp::services

View File

@@ -66,6 +66,12 @@ public:
*/
virtual const MouseGrabConfig& GetMouseGrabConfig() const = 0;
/**
* @brief Get render graph settings.
* @return Render graph configuration
*/
virtual const RenderGraphConfig& GetRenderGraphConfig() const = 0;
/**
* @brief Get the full JSON configuration as a string.
*

View File

@@ -61,6 +61,18 @@ public:
*/
virtual void LoadShaders(const std::unordered_map<std::string, ShaderPaths>& shaders) = 0;
/**
* @brief Store the Lua-defined render graph definition.
*
* @param definition Render graph definition
*/
virtual void SetRenderGraphDefinition(const RenderGraphDefinition& definition) = 0;
/**
* @brief Get the current render graph definition.
*/
virtual const RenderGraphDefinition& GetRenderGraphDefinition() const = 0;
/**
* @brief Upload vertex data to GPU buffer.
*

View File

@@ -0,0 +1,30 @@
#pragma once
#include "graphics_types.hpp"
namespace sdl3cpp::services {
/**
* @brief Loads render graph definitions from Lua.
*/
class IRenderGraphScriptService {
public:
virtual ~IRenderGraphScriptService() = default;
/**
* @brief Check if render graph execution is enabled.
*/
virtual bool IsEnabled() const = 0;
/**
* @brief Check whether the configured Lua function exists.
*/
virtual bool HasRenderGraphFunction() const = 0;
/**
* @brief Load the render graph definition from Lua.
*/
virtual RenderGraphDefinition LoadRenderGraph() = 0;
};
} // namespace sdl3cpp::services

View File

@@ -40,3 +40,19 @@ end
function compute_model_matrix(time)
return identity_matrix()
end
function get_render_graph()
return {
resources = {
scene_hdr = {type = "color", format = "rgba16f", size = "swapchain"},
depth = {type = "depth", format = "d32", size = "swapchain"},
},
passes = {
{
name = "gbuffer",
kind = "gbuffer",
outputs = {color = "scene_hdr", depth = "depth"},
},
},
}
end