From 01f1e9499477470dd19e5744fb4390928def802d Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Tue, 6 Jan 2026 00:14:00 +0000 Subject: [PATCH] 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. --- config/seed_runtime.json | 5 + config/vita_cube_runtime.json | 6 +- scripts/cube_logic.lua | 122 ++++- src/app/service_based_app.cpp | 6 + src/services/impl/graphics_service.cpp | 13 + src/services/impl/graphics_service.hpp | 5 +- src/services/impl/json_config_service.cpp | 31 ++ src/services/impl/json_config_service.hpp | 6 + src/services/impl/render_command_service.cpp | 2 +- .../impl/render_coordinator_service.cpp | 18 + .../impl/render_coordinator_service.hpp | 4 + .../impl/render_graph_script_service.cpp | 417 ++++++++++++++++++ .../impl/render_graph_script_service.hpp | 43 ++ src/services/interfaces/config_types.hpp | 10 + src/services/interfaces/graphics_types.hpp | 56 ++- src/services/interfaces/i_config_service.hpp | 6 + .../interfaces/i_graphics_service.hpp | 12 + .../i_render_graph_script_service.hpp | 30 ++ tests/scripts/unit_cube_logic.lua | 16 + 19 files changed, 795 insertions(+), 13 deletions(-) create mode 100644 src/services/impl/render_graph_script_service.cpp create mode 100644 src/services/impl/render_graph_script_service.hpp create mode 100644 src/services/interfaces/i_render_graph_script_service.hpp diff --git a/config/seed_runtime.json b/config/seed_runtime.json index 74403c2..a82bb2b 100644 --- a/config/seed_runtime.json +++ b/config/seed_runtime.json @@ -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" } diff --git a/config/vita_cube_runtime.json b/config/vita_cube_runtime.json index 34c23f8..22039bb 100644 --- a/config/vita_cube_runtime.json +++ b/config/vita_cube_runtime.json @@ -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 } -} \ No newline at end of file +} diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index 1b54434..7de9231 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -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 diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index b1ab86d..7d92e24 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -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( registry_.GetService(), registry_.GetService()); + registry_.RegisterService( + registry_.GetService(), + registry_.GetService(), + registry_.GetService()); registry_.RegisterService( registry_.GetService(), registry_.GetService()); @@ -356,6 +361,7 @@ void ServiceBasedApp::RegisterServices() { registry_.GetService(), registry_.GetService(), registry_.GetService(), + registry_.GetService(), registry_.GetService(), registry_.GetService(), registry_.GetService()); diff --git a/src/services/impl/graphics_service.cpp b/src/services/impl/graphics_service.cpp index 5e9a51e..79f72da 100644 --- a/src/services/impl/graphics_service.cpp +++ b/src/services/impl/graphics_service.cpp @@ -106,6 +106,19 @@ void GraphicsService::LoadShaders(const std::unordered_mapTrace("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& vertices) { logger_->Trace("GraphicsService", "UploadVertexData", "vertices.size=" + std::to_string(vertices.size())); diff --git a/src/services/impl/graphics_service.hpp b/src/services/impl/graphics_service.hpp index b30fb34..83f91dc 100644 --- a/src/services/impl/graphics_service.hpp +++ b/src/services/impl/graphics_service.hpp @@ -34,6 +34,8 @@ public: void InitializeSwapchain() override; void RecreateSwapchain() override; void LoadShaders(const std::unordered_map& shaders) override; + void SetRenderGraphDefinition(const RenderGraphDefinition& definition) override; + const RenderGraphDefinition& GetRenderGraphDefinition() const override; void UploadVertexData(const std::vector& vertices) override; void UploadIndexData(const std::vector& indices) override; bool BeginFrame() override; @@ -56,8 +58,9 @@ private: std::unordered_map pipelines_; GraphicsBufferHandle vertexBuffer_; GraphicsBufferHandle indexBuffer_; + RenderGraphDefinition renderGraphDefinition_{}; // Other state bool initialized_ = false; }; -} // namespace sdl3cpp::services::impl \ No newline at end of file +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index 2df1aa5..cb5d26f 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -353,6 +353,7 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr 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 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); diff --git a/src/services/impl/json_config_service.hpp b/src/services/impl/json_config_service.hpp index c735883..acf8a32 100644 --- a/src/services/impl/json_config_service.hpp +++ b/src/services/impl/json_config_service.hpp @@ -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"); diff --git a/src/services/impl/render_command_service.cpp b/src/services/impl/render_command_service.cpp index b01b41c..6bad2d4 100644 --- a/src/services/impl/render_command_service.cpp +++ b/src/services/impl/render_command_service.cpp @@ -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 } diff --git a/src/services/impl/render_coordinator_service.cpp b/src/services/impl/render_coordinator_service.cpp index a70babc..3247c32 100644 --- a/src/services/impl/render_coordinator_service.cpp +++ b/src/services/impl/render_coordinator_service.cpp @@ -6,6 +6,7 @@ RenderCoordinatorService::RenderCoordinatorService(std::shared_ptr logg std::shared_ptr graphicsService, std::shared_ptr sceneScriptService, std::shared_ptr shaderScriptService, + std::shared_ptr renderGraphScriptService, std::shared_ptr guiScriptService, std::shared_ptr guiService, std::shared_ptr sceneService) @@ -13,6 +14,7 @@ RenderCoordinatorService::RenderCoordinatorService(std::shared_ptr 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 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_) { diff --git a/src/services/impl/render_coordinator_service.hpp b/src/services/impl/render_coordinator_service.hpp index d85eab8..22d54ac 100644 --- a/src/services/impl/render_coordinator_service.hpp +++ b/src/services/impl/render_coordinator_service.hpp @@ -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 graphicsService, std::shared_ptr sceneScriptService, std::shared_ptr shaderScriptService, + std::shared_ptr renderGraphScriptService, std::shared_ptr guiScriptService, std::shared_ptr guiService, std::shared_ptr sceneService); @@ -30,6 +32,7 @@ private: std::shared_ptr graphicsService_; std::shared_ptr sceneScriptService_; std::shared_ptr shaderScriptService_; + std::shared_ptr renderGraphScriptService_; std::shared_ptr guiScriptService_; std::shared_ptr guiService_; std::shared_ptr sceneService_; @@ -37,6 +40,7 @@ private: size_t lastIndexCount_ = 0; bool shadersLoaded_ = false; bool geometryUploaded_ = false; + bool renderGraphInitialized_ = false; }; } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/render_graph_script_service.cpp b/src/services/impl/render_graph_script_service.cpp new file mode 100644 index 0000000..016ffb6 --- /dev/null +++ b/src/services/impl/render_graph_script_service.cpp @@ -0,0 +1,417 @@ +#include "render_graph_script_service.hpp" + +#include "lua_helpers.hpp" +#include "services/interfaces/i_logger.hpp" + +#include + +#include +#include +#include + +namespace sdl3cpp::services::impl { + +RenderGraphScriptService::RenderGraphScriptService(std::shared_ptr engineService, + std::shared_ptr configService, + std::shared_ptr 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(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(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(lua_tonumber(L, -2)); + resource.explicitSize[1] = static_cast(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(lua_tonumber(L, -2)); + resource.explicitSize[1] = static_cast(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 RenderGraphScriptService::ParseStringMap(lua_State* L, + int index) const { + std::unordered_map 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 RenderGraphScriptService::ParseSettingsMap(lua_State* L, + int index) const { + std::unordered_map 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(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 diff --git a/src/services/impl/render_graph_script_service.hpp b/src/services/impl/render_graph_script_service.hpp new file mode 100644 index 0000000..927d602 --- /dev/null +++ b/src/services/impl/render_graph_script_service.hpp @@ -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 + +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 engineService, + std::shared_ptr configService, + std::shared_ptr 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 ParseStringMap(lua_State* L, int index) const; + std::unordered_map ParseSettingsMap(lua_State* L, int index) const; + + std::shared_ptr engineService_; + std::shared_ptr configService_; + std::shared_ptr logger_; + bool graphLoaded_ = false; + RenderGraphDefinition cachedGraph_{}; +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/interfaces/config_types.hpp b/src/services/interfaces/config_types.hpp index 9bf9ff4..3073b5e 100644 --- a/src/services/interfaces/config_types.hpp +++ b/src/services/interfaces/config_types.hpp @@ -71,6 +71,7 @@ struct AtmosphericsConfig { float fogDensity = 0.003f; std::array 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; }; diff --git a/src/services/interfaces/graphics_types.hpp b/src/services/interfaces/graphics_types.hpp index d5ffd52..57ab7ea 100644 --- a/src/services/interfaces/graphics_types.hpp +++ b/src/services/interfaces/graphics_types.hpp @@ -1,9 +1,10 @@ #pragma once +#include #include #include +#include #include -#include namespace sdl3cpp::services { @@ -45,4 +46,57 @@ struct RenderCommand { std::array 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 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 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 inputs; + std::unordered_map outputs; + std::unordered_map settings; +}; + +/** + * @brief Render graph definition loaded from Lua. + */ +struct RenderGraphDefinition { + std::vector resources; + std::vector passes; +}; + } // namespace sdl3cpp::services diff --git a/src/services/interfaces/i_config_service.hpp b/src/services/interfaces/i_config_service.hpp index 122e852..d3c1198 100644 --- a/src/services/interfaces/i_config_service.hpp +++ b/src/services/interfaces/i_config_service.hpp @@ -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. * diff --git a/src/services/interfaces/i_graphics_service.hpp b/src/services/interfaces/i_graphics_service.hpp index 0a28e84..85a4445 100644 --- a/src/services/interfaces/i_graphics_service.hpp +++ b/src/services/interfaces/i_graphics_service.hpp @@ -61,6 +61,18 @@ public: */ virtual void LoadShaders(const std::unordered_map& 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. * diff --git a/src/services/interfaces/i_render_graph_script_service.hpp b/src/services/interfaces/i_render_graph_script_service.hpp new file mode 100644 index 0000000..89c30d6 --- /dev/null +++ b/src/services/interfaces/i_render_graph_script_service.hpp @@ -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 diff --git a/tests/scripts/unit_cube_logic.lua b/tests/scripts/unit_cube_logic.lua index eb4cdfb..047625f 100644 --- a/tests/scripts/unit_cube_logic.lua +++ b/tests/scripts/unit_cube_logic.lua @@ -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