diff --git a/CMakeLists.txt b/CMakeLists.txt index 50793b5..5e67168 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,6 +124,8 @@ add_library(render_renderer STATIC engine/render/lighting.hpp engine/render/lighting.cpp engine/render/texture_types.hpp + engine/render/render_pass_system.hpp + engine/render/render_pass_system.cpp engine/render/vfx.hpp engine/render/vfx.cpp engine/render/bgfx/renderer_bgfx.cpp @@ -228,6 +230,12 @@ if(RENDER_BUILD_TESTS) render_apply_project_options(render_debug_renderer_tests) render_apply_warnings(render_debug_renderer_tests) + + add_executable(render_render_pass_system_tests tests/render/render_pass_system_tests.cpp) + target_link_libraries(render_render_pass_system_tests PRIVATE render::renderer) + render_apply_project_options(render_render_pass_system_tests) + render_apply_warnings(render_render_pass_system_tests) + add_executable(render_scene_tests tests/scene/scene_tests.cpp) target_link_libraries(render_scene_tests PRIVATE render::scene) render_apply_project_options(render_scene_tests) @@ -256,6 +264,8 @@ if(RENDER_BUILD_TESTS) set_tests_properties(unit.renderer.lighting_pipeline PROPERTIES LABELS "unit;renderer;lighting") add_test(NAME unit.renderer.debug_views COMMAND render_debug_renderer_tests) set_tests_properties(unit.renderer.debug_views PROPERTIES LABELS "unit;renderer;debug") + add_test(NAME unit.renderer.render_pass_system COMMAND render_render_pass_system_tests) + set_tests_properties(unit.renderer.render_pass_system PROPERTIES LABELS "unit;renderer;graph") add_test(NAME unit.scene.runtime COMMAND render_scene_tests) set_tests_properties(unit.scene.runtime PROPERTIES LABELS "unit;scene") add_test(NAME unit.renderer.vfx_system COMMAND render_vfx_system_tests) diff --git a/docs/rendering.md b/docs/rendering.md index 58ae773..206a0f6 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -268,3 +268,56 @@ Highlights: - shader pipeline integration through `vfx/ambient_sprite` manifest entries and variants Detailed design and current limits are documented in `docs/vfx.md`. + +## Structured render pass system (Statement 16) + +Statement 16 adds a lightweight, engine-owned render graph under `engine/render/render_pass_system.*`. + +### Core model + +- `RenderPassRegistry` owns per-frame pass/resource declarations and orchestration. +- `RenderPassDefinition` encodes pass name, dependencies, explicit resource usage, optional enable predicate, and execution callback. +- `RenderResourceDesc` encodes frame resources (shadow map, scene color, bloom intermediates, UI/debug overlays, imported backbuffer). +- `RenderPassExecutionContext` passes frame metadata, renderer access, registry access, and user payload pointer into pass execution. + +### Validation and ordering + +Build-time checks fail clearly for: + +- duplicate pass/resource names +- missing pass dependencies +- undeclared resource usage +- resource write conflicts +- reads with no producer (unless resource is imported) +- dependency cycles + +Execution order is resolved by topological sorting over explicit `depends_on` edges plus inferred producer->consumer edges from resource reads. + +### Current shell frame layout + +`engine/shell/main.cpp` now assembles the frame through the pass system, with explicit passes: + +1. `shadow-pass` +2. `main-lit-pass` +3. `bloom-extract-pass` +4. `bloom-blur-pass` +5. `bloom-composite-pass` +6. `outline-pass` +7. `ui-pass` +8. `debug-pass` +9. `present-pass` + +This removes ad hoc scattered manual ordering and creates a single inspectable orchestration path. + +### Diagnostics + +- `RenderPassRegistry::dump_graph()` provides a textual pass/resource dump for logs. +- `RenderPassDiagnostics` exposes execution order and active/inactive pass sets. +- Pass execution integrates with renderer debug timing rows (`RendererPassTiming`) for per-pass CPU visibility. + +### Deferred follow-ups + +- physical render-target allocation/aliasing and transient memory optimization +- backend GPU timer query integration for real GPU timings +- richer pass-level debug UI (resource producers/consumers overlay) +- optional pass plugin registration for editor/runtime extension modules diff --git a/engine/render/README.md b/engine/render/README.md index 6ed99f6..c783d76 100644 --- a/engine/render/README.md +++ b/engine/render/README.md @@ -89,3 +89,16 @@ Current debug view behavior: - category toggles (`VfxToggleMask`) for profiling and debug isolation Scene-level attachment is handled by `scene::VfxAttachmentComponent` (in `engine/scene`) so effects can follow node transforms without exposing backend-specific handles. + +## Structured render-pass orchestration (Statement 16) + +`render_pass_system.hpp/.cpp` introduces the canonical engine-owned pass orchestration layer. + +- Passes are first-class definitions (`RenderPassDefinition`) with a stable name, dependency list, explicit resource usage declarations, enable predicates, and execute hooks. +- Frame resources are explicitly declared through `RenderResourceDesc` (`RenderTarget`, `DepthTarget`, `ShadowMap`, post-process textures, UI targets, and imported `Backbuffer`). +- Build-time validation checks undeclared resources, missing dependencies, duplicate names, write conflicts, read-without-producer misuse, and dependency cycles. +- Build step computes a deterministic execution order from explicit dependencies plus producer->consumer resource edges. +- Runtime execution tracks active/inactive passes and emits pass timing rows through renderer debug timing hooks. +- `dump_graph()` exposes a human-readable frame graph/resource layout for logs and inspection. + +This is intentionally a lightweight engine-owned render graph: explicit and extensible now, without introducing a heavyweight AAA framegraph scheduler. diff --git a/engine/render/render_pass_system.cpp b/engine/render/render_pass_system.cpp new file mode 100644 index 0000000..91197d0 --- /dev/null +++ b/engine/render/render_pass_system.cpp @@ -0,0 +1,330 @@ +#include "engine/render/render_pass_system.hpp" + +#include +#include +#include +#include + +namespace render::rendering { +namespace { + +bool is_read_access(const RenderResourceAccess access) { + return access == RenderResourceAccess::Read || access == RenderResourceAccess::ReadWrite; +} + +bool is_write_access(const RenderResourceAccess access) { + return access == RenderResourceAccess::Write || access == RenderResourceAccess::ReadWrite; +} + +} // namespace + +void RenderPassRegistry::reset() { + resources_.clear(); + resource_index_.clear(); + passes_.clear(); + pass_index_.clear(); + execution_order_.clear(); + resource_producers_.clear(); + diagnostics_ = {}; + built_ = false; +} + +bool RenderPassRegistry::declare_resource(const RenderResourceDesc& resource, std::string* error) { + if (resource.name.empty()) { + if (error != nullptr) { + *error = "resource name must not be empty"; + } + return false; + } + if (resource_index_.contains(resource.name)) { + if (error != nullptr) { + *error = "resource already declared: " + resource.name; + } + return false; + } + + resource_index_.emplace(resource.name, resources_.size()); + resources_.push_back(resource); + return true; +} + +bool RenderPassRegistry::add_pass(RenderPassDefinition pass, std::string* error) { + if (pass.name.empty()) { + if (error != nullptr) { + *error = "pass name must not be empty"; + } + return false; + } + if (pass_index_.contains(pass.name)) { + if (error != nullptr) { + *error = "duplicate pass name: " + pass.name; + } + return false; + } + if (!pass.execute) { + if (error != nullptr) { + *error = "pass execute callback missing: " + pass.name; + } + return false; + } + + pass_index_.emplace(pass.name, passes_.size()); + passes_.push_back(PassNode{.definition = std::move(pass)}); + return true; +} + +RenderGraphBuildResult RenderPassRegistry::build() { + std::string error; + if (!validate(&error) || !resolve_execution_order(&error)) { + built_ = false; + return RenderGraphBuildResult{.ok = false, .error = error}; + } + built_ = true; + return RenderGraphBuildResult{.ok = true, .error = {}}; +} + +bool RenderPassRegistry::execute(const RenderPassExecutionContext& context, std::string* error) { + if (!built_) { + if (error != nullptr) { + *error = "render pass registry execute called before successful build"; + } + return false; + } + + diagnostics_.active_passes.clear(); + diagnostics_.inactive_passes.clear(); + + for (const std::string& pass_name : execution_order_) { + const auto pass_it = pass_index_.find(pass_name); + if (pass_it == pass_index_.end()) { + continue; + } + + const RenderPassDefinition& pass = passes_[pass_it->second].definition; + const bool enabled = pass.enabled ? pass.enabled(context) : true; + if (!enabled) { + diagnostics_.inactive_passes.push_back(pass_name); + continue; + } + + diagnostics_.active_passes.push_back(pass_name); + + const auto pass_begin = std::chrono::steady_clock::now(); + pass.execute(context); + const auto pass_end = std::chrono::steady_clock::now(); + + if (context.renderer != nullptr) { + const float cpu_ms = static_cast(std::chrono::duration(pass_end - pass_begin).count()); + context.renderer->add_debug_pass_timing(RendererPassTiming{.pass_name = pass.name, .cpu_ms = cpu_ms, .gpu_ms = std::nullopt}); + } + } + + return true; +} + +std::optional RenderPassRegistry::find_resource(const std::string_view name) const { + const auto it = resource_index_.find(std::string{name}); + if (it == resource_index_.end()) { + return std::nullopt; + } + return resources_[it->second]; +} + +std::span RenderPassRegistry::resources() const noexcept { + return resources_; +} + +std::span RenderPassRegistry::ordered_passes() const noexcept { + return execution_order_; +} + +const RenderPassDiagnostics& RenderPassRegistry::diagnostics() const noexcept { + return diagnostics_; +} + +std::string RenderPassRegistry::dump_graph() const { + std::ostringstream out; + out << "render-pass-graph\n"; + out << "resources:\n"; + for (const auto& resource : resources_) { + out << " - " << resource.name << " [" << static_cast(resource.kind) << "] " + << resource.width << "x" << resource.height + << (resource.transient ? " transient" : " persistent") + << (resource.imported ? " imported" : "") << "\n"; + } + + out << "passes:\n"; + for (const auto& pass_name : execution_order_) { + const auto pass_it = pass_index_.find(pass_name); + if (pass_it == pass_index_.end()) { + continue; + } + const auto& pass = passes_[pass_it->second].definition; + out << " - " << pass.name; + if (!pass.depends_on.empty()) { + out << " depends=["; + for (std::size_t i = 0; i < pass.depends_on.size(); ++i) { + out << pass.depends_on[i]; + if (i + 1 < pass.depends_on.size()) { + out << ", "; + } + } + out << "]"; + } + out << " resources=["; + for (std::size_t i = 0; i < pass.resources.size(); ++i) { + out << pass.resources[i].resource << ":" << static_cast(pass.resources[i].access); + if (i + 1 < pass.resources.size()) { + out << ", "; + } + } + out << "]\n"; + } + + return out.str(); +} + +bool RenderPassRegistry::validate(std::string* error) { + diagnostics_.validation_messages.clear(); + resource_producers_.clear(); + + for (const PassNode& node : passes_) { + const RenderPassDefinition& pass = node.definition; + + for (const std::string& dep : pass.depends_on) { + if (!pass_index_.contains(dep)) { + const std::string msg = "pass '" + pass.name + "' depends on missing pass '" + dep + "'"; + diagnostics_.validation_messages.push_back(msg); + if (error != nullptr) { + *error = msg; + } + return false; + } + } + + for (const RenderPassResourceUsage& usage : pass.resources) { + if (!resource_index_.contains(usage.resource)) { + const std::string msg = "pass '" + pass.name + "' references undeclared resource '" + usage.resource + "'"; + diagnostics_.validation_messages.push_back(msg); + if (error != nullptr) { + *error = msg; + } + return false; + } + + if (is_write_access(usage.access)) { + const auto producer_it = resource_producers_.find(usage.resource); + if (producer_it != resource_producers_.end()) { + const std::string msg = "resource '" + usage.resource + "' already has producer '" + producer_it->second + + "', cannot also be written by pass '" + pass.name + "'"; + diagnostics_.validation_messages.push_back(msg); + if (error != nullptr) { + *error = msg; + } + return false; + } + resource_producers_.emplace(usage.resource, pass.name); + } + } + } + + for (const PassNode& node : passes_) { + const RenderPassDefinition& pass = node.definition; + for (const RenderPassResourceUsage& usage : pass.resources) { + if (!is_read_access(usage.access)) { + continue; + } + + const auto resource_it = resource_index_.find(usage.resource); + if (resource_it == resource_index_.end()) { + continue; + } + const RenderResourceDesc& resource = resources_[resource_it->second]; + if (resource.imported) { + continue; + } + + if (!resource_producers_.contains(usage.resource)) { + const std::string msg = "pass '" + pass.name + "' reads resource '" + usage.resource + "' with no producing pass"; + diagnostics_.validation_messages.push_back(msg); + if (error != nullptr) { + *error = msg; + } + return false; + } + } + } + + return true; +} + +bool RenderPassRegistry::resolve_execution_order(std::string* error) { + enum class VisitState : std::uint8_t { Unvisited = 0, Visiting, Visited }; + + std::unordered_map> dependency_graph; + dependency_graph.reserve(passes_.size()); + + for (const PassNode& node : passes_) { + const RenderPassDefinition& pass = node.definition; + auto& deps = dependency_graph[pass.name]; + deps = pass.depends_on; + + for (const RenderPassResourceUsage& usage : pass.resources) { + if (!is_read_access(usage.access)) { + continue; + } + const auto producer_it = resource_producers_.find(usage.resource); + if (producer_it == resource_producers_.end()) { + continue; + } + if (producer_it->second != pass.name) { + deps.push_back(producer_it->second); + } + } + + std::sort(deps.begin(), deps.end()); + deps.erase(std::unique(deps.begin(), deps.end()), deps.end()); + } + + std::unordered_map state; + state.reserve(passes_.size()); + execution_order_.clear(); + execution_order_.reserve(passes_.size()); + + std::function visit = [&](const std::string& pass_name) -> bool { + const VisitState current = state[pass_name]; + if (current == VisitState::Visited) { + return true; + } + if (current == VisitState::Visiting) { + const std::string msg = "dependency cycle detected at pass '" + pass_name + "'"; + diagnostics_.validation_messages.push_back(msg); + if (error != nullptr) { + *error = msg; + } + return false; + } + + state[pass_name] = VisitState::Visiting; + for (const std::string& dep : dependency_graph[pass_name]) { + if (!visit(dep)) { + return false; + } + } + + state[pass_name] = VisitState::Visited; + execution_order_.push_back(pass_name); + return true; + }; + + for (const PassNode& node : passes_) { + if (!visit(node.definition.name)) { + return false; + } + } + + diagnostics_.execution_order = execution_order_; + return true; +} + +} // namespace render::rendering diff --git a/engine/render/render_pass_system.hpp b/engine/render/render_pass_system.hpp new file mode 100644 index 0000000..4886c25 --- /dev/null +++ b/engine/render/render_pass_system.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include "engine/render/renderer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace render::rendering { + +enum class RenderResourceKind : std::uint8_t { + RenderTarget = 0, + DepthTarget, + ShadowMap, + PostProcessTexture, + UiTarget, + Backbuffer, +}; + +enum class RenderResourceAccess : std::uint8_t { + Read = 0, + Write, + ReadWrite, +}; + +struct RenderResourceDesc { + std::string name{}; + RenderResourceKind kind{RenderResourceKind::RenderTarget}; + std::uint16_t width{0}; + std::uint16_t height{0}; + bool transient{true}; + bool imported{false}; +}; + +struct RenderPassResourceUsage { + std::string resource{}; + RenderResourceAccess access{RenderResourceAccess::Read}; + bool clear_before_use{false}; +}; + +struct RenderFrameInfo { + std::uint64_t frame_index{0}; + std::uint16_t backbuffer_width{0}; + std::uint16_t backbuffer_height{0}; + float delta_seconds{0.0F}; +}; + +class RenderPassRegistry; + +struct RenderPassExecutionContext { + RenderFrameInfo frame{}; + Renderer* renderer{nullptr}; + const RenderPassRegistry* registry{nullptr}; + void* user_data{nullptr}; +}; + +using RenderPassExecuteFn = std::function; +using RenderPassEnabledFn = std::function; + +struct RenderPassDefinition { + std::string name{}; + std::vector depends_on{}; + std::vector resources{}; + RenderPassExecuteFn execute{}; + RenderPassEnabledFn enabled{}; +}; + +struct RenderPassDiagnostics { + std::vector execution_order{}; + std::vector active_passes{}; + std::vector inactive_passes{}; + std::vector validation_messages{}; +}; + +struct RenderGraphBuildResult { + bool ok{false}; + std::string error{}; +}; + +class RenderPassRegistry { +public: + void reset(); + + [[nodiscard]] bool declare_resource(const RenderResourceDesc& resource, std::string* error = nullptr); + [[nodiscard]] bool add_pass(RenderPassDefinition pass, std::string* error = nullptr); + + [[nodiscard]] RenderGraphBuildResult build(); + [[nodiscard]] bool execute(const RenderPassExecutionContext& context, std::string* error = nullptr); + + [[nodiscard]] std::optional find_resource(std::string_view name) const; + [[nodiscard]] std::span resources() const noexcept; + [[nodiscard]] std::span ordered_passes() const noexcept; + [[nodiscard]] const RenderPassDiagnostics& diagnostics() const noexcept; + [[nodiscard]] std::string dump_graph() const; + +private: + struct PassNode { + RenderPassDefinition definition{}; + }; + + [[nodiscard]] bool validate(std::string* error); + [[nodiscard]] bool resolve_execution_order(std::string* error); + + std::vector resources_{}; + std::unordered_map resource_index_{}; + + std::vector passes_{}; + std::unordered_map pass_index_{}; + + std::vector execution_order_{}; + std::unordered_map resource_producers_{}; + RenderPassDiagnostics diagnostics_{}; + bool built_{false}; +}; + +} // namespace render::rendering diff --git a/engine/shell/main.cpp b/engine/shell/main.cpp index 1c471d4..1fb651a 100644 --- a/engine/shell/main.cpp +++ b/engine/shell/main.cpp @@ -4,6 +4,7 @@ #include "engine/render/draw_submission.hpp" #include "engine/render/lighting.hpp" #include "engine/render/renderer.hpp" +#include "engine/render/render_pass_system.hpp" #include "engine/render/shader_library.hpp" #include "engine/render/vfx.hpp" #include "engine/scene/scene.hpp" @@ -244,6 +245,9 @@ int main(int argc, char** argv) { scene.set_vfx_attachment(market_node, {.effect_handle = handle.value}); } + render::rendering::RenderPassRegistry pass_registry; + bool logged_graph = false; + while (!runtime.should_quit()) { runtime.begin_frame(); runtime.pump_events(); @@ -285,10 +289,20 @@ int main(int argc, char** argv) { } } - std::vector submissions; + struct ShellFrameState { + std::vector submissions{}; + std::vector directional_lights{}; + std::vector point_lights{}; + std::vector lit_inputs{}; + render::rendering::LightingFrameData lighting_frame{}; + render::rendering::SubmissionDiagnostics submission_diagnostics{}; + render::rendering::vfx::EffectDiagnostics vfx_diagnostics{}; + bool built_lighting_frame{false}; + } frame_state; + const auto visible = scene.collect_visible_renderables(); const auto visible_lights = scene.collect_visible_lights(); - submissions.reserve(visible.size()); + frame_state.submissions.reserve(visible.size()); for (const auto& vr : visible) { if (vr.renderable == nullptr || vr.world_transform == nullptr || !vr.renderable->material.valid()) continue; const auto world = to_array(vr.world_transform->to_matrix()); @@ -301,25 +315,23 @@ int main(int argc, char** argv) { draw.draw_state = vr.renderable->draw_state; draw.transform = world; draw.sort_key = static_cast(vr.node.index); - submissions.push_back(draw); + frame_state.submissions.push_back(draw); } - std::vector directional_lights; - std::vector point_lights; for (const auto& visible_light : visible_lights) { if (visible_light.light == nullptr || visible_light.world_transform == nullptr) { continue; } if (visible_light.light->type == render::scene::LightType::Directional) { const render::core::Vec3 direction = render::core::forward(*visible_light.world_transform) * -1.0F; - directional_lights.push_back(render::rendering::DirectionalLightInput{ + frame_state.directional_lights.push_back(render::rendering::DirectionalLightInput{ .direction = direction, .color = visible_light.light->color, .intensity = visible_light.light->intensity, .casts_shadows = visible_light.light->casts_shadows, }); } else { - point_lights.push_back(render::rendering::PointLightInput{ + frame_state.point_lights.push_back(render::rendering::PointLightInput{ .position = visible_light.world_transform->translation, .color = visible_light.light->color, .intensity = visible_light.light->intensity, @@ -329,13 +341,12 @@ int main(int argc, char** argv) { } } - std::vector lit_inputs; - lit_inputs.reserve(visible.size()); + frame_state.lit_inputs.reserve(visible.size()); for (const auto& vr : visible) { if (vr.world_transform == nullptr || vr.renderable == nullptr) { continue; } - lit_inputs.push_back(render::rendering::RenderableLightingInput{ + frame_state.lit_inputs.push_back(render::rendering::RenderableLightingInput{ .object_id = vr.node.index, .position = vr.world_transform->translation, .bounding_radius = 1.0F, @@ -355,73 +366,192 @@ int main(int argc, char** argv) { bloom.threshold = scene.lighting_settings().bloom.threshold; bloom.intensity = scene.lighting_settings().bloom.intensity; - const auto shadow_start = std::chrono::steady_clock::now(); - const render::rendering::LightingFrameData lighting_frame = render::rendering::build_forward_plus_frame_data( - camera_position, - directional_lights, - point_lights, - lit_inputs, - {}, - fog, - bloom, - {}, - {}); - const auto shadow_end = std::chrono::steady_clock::now(); - renderer.add_debug_pass_timing(render::rendering::RendererPassTiming{ - .pass_name = "shadow-pass(plan)", - .cpu_ms = static_cast(std::chrono::duration(shadow_end - shadow_start).count()), - .gpu_ms = std::nullopt, - }); - (void)lighting_frame; - - const auto main_pass_start = std::chrono::steady_clock::now(); - render::rendering::SubmissionDiagnostics diagnostics{}; - const auto batches = render::rendering::build_draw_batches(submissions, {}, &diagnostics); - for (const auto& batch : batches) { - if (batch.mode == render::rendering::SubmissionMode::Instanced) { - render::rendering::DrawSubmission base{}; - base.view = kMainView; - base.mesh = batch.mesh; - base.material = batch.material; - base.draw_state = batch.draw_state; - base.transform = {}; - renderer.submit_instanced(kMainView, base, std::span{batch.transforms}); - } else if (!batch.unique_draws.empty()) { - renderer.submit(kMainView, batch.unique_draws.front()); - } + pass_registry.reset(); + std::string graph_error; + pass_registry.declare_resource({.name = "shadow_map", .kind = render::rendering::RenderResourceKind::ShadowMap, .width = 2048, .height = 2048, .transient = true, .imported = false}, &graph_error); + pass_registry.declare_resource({.name = "scene_color", .kind = render::rendering::RenderResourceKind::RenderTarget, .width = static_cast(window.width), .height = static_cast(window.height), .transient = true, .imported = false}, &graph_error); + pass_registry.declare_resource({.name = "bloom_extract", .kind = render::rendering::RenderResourceKind::PostProcessTexture, .width = static_cast(window.width / 2U), .height = static_cast(window.height / 2U), .transient = true, .imported = false}, &graph_error); + pass_registry.declare_resource({.name = "bloom_blur", .kind = render::rendering::RenderResourceKind::PostProcessTexture, .width = static_cast(window.width / 2U), .height = static_cast(window.height / 2U), .transient = true, .imported = false}, &graph_error); + pass_registry.declare_resource({.name = "post_color", .kind = render::rendering::RenderResourceKind::PostProcessTexture, .width = static_cast(window.width), .height = static_cast(window.height), .transient = true, .imported = false}, &graph_error); + pass_registry.declare_resource({.name = "outline_mask", .kind = render::rendering::RenderResourceKind::PostProcessTexture, .width = static_cast(window.width), .height = static_cast(window.height), .transient = true, .imported = false}, &graph_error); + pass_registry.declare_resource({.name = "ui_overlay", .kind = render::rendering::RenderResourceKind::UiTarget, .width = static_cast(window.width), .height = static_cast(window.height), .transient = true, .imported = false}, &graph_error); + pass_registry.declare_resource({.name = "debug_overlay", .kind = render::rendering::RenderResourceKind::UiTarget, .width = static_cast(window.width), .height = static_cast(window.height), .transient = true, .imported = false}, &graph_error); + pass_registry.declare_resource({.name = "backbuffer", .kind = render::rendering::RenderResourceKind::Backbuffer, .width = static_cast(window.width), .height = static_cast(window.height), .transient = false, .imported = true}, &graph_error); + + pass_registry.add_pass(render::rendering::RenderPassDefinition{ + .name = "shadow-pass", + .depends_on = {}, + .resources = {{.resource = "shadow_map", .access = render::rendering::RenderResourceAccess::Write, .clear_before_use = true}}, + .execute = [&](const render::rendering::RenderPassExecutionContext&) { + frame_state.lighting_frame = render::rendering::build_forward_plus_frame_data( + camera_position, + frame_state.directional_lights, + frame_state.point_lights, + frame_state.lit_inputs, + {}, + fog, + bloom, + {}, + {}); + frame_state.built_lighting_frame = true; + }, + .enabled = {}, + }, &graph_error); + + pass_registry.add_pass(render::rendering::RenderPassDefinition{ + .name = "main-lit-pass", + .depends_on = {"shadow-pass"}, + .resources = { + {.resource = "shadow_map", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "scene_color", .access = render::rendering::RenderResourceAccess::Write, .clear_before_use = true}, + }, + .execute = [&](const render::rendering::RenderPassExecutionContext&) { + frame_state.submission_diagnostics = {}; + const auto batches = render::rendering::build_draw_batches(frame_state.submissions, {}, &frame_state.submission_diagnostics); + for (const auto& batch : batches) { + if (batch.mode == render::rendering::SubmissionMode::Instanced) { + render::rendering::DrawSubmission base{}; + base.view = kMainView; + base.mesh = batch.mesh; + base.material = batch.material; + base.draw_state = batch.draw_state; + base.transform = {}; + renderer.submit_instanced(kMainView, base, std::span{batch.transforms}); + } else if (!batch.unique_draws.empty()) { + renderer.submit(kMainView, batch.unique_draws.front()); + } + } + + auto vfx_submissions = vfx_system.build_frame_submissions(kMainView); + for (const auto& vfx : vfx_submissions) { + renderer.submit_instanced(kMainView, vfx.draw, std::span{vfx.transforms}); + } + frame_state.vfx_diagnostics = vfx_system.diagnostics(); + }, + .enabled = {}, + }, &graph_error); + + pass_registry.add_pass(render::rendering::RenderPassDefinition{ + .name = "bloom-extract-pass", + .depends_on = {"main-lit-pass"}, + .resources = { + {.resource = "scene_color", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "bloom_extract", .access = render::rendering::RenderResourceAccess::Write, .clear_before_use = true}, + }, + .execute = [](const render::rendering::RenderPassExecutionContext&) {}, + .enabled = [bloom](const render::rendering::RenderPassExecutionContext&) { return bloom.enabled; }, + }, &graph_error); + + pass_registry.add_pass(render::rendering::RenderPassDefinition{ + .name = "bloom-blur-pass", + .depends_on = {"bloom-extract-pass"}, + .resources = { + {.resource = "bloom_extract", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "bloom_blur", .access = render::rendering::RenderResourceAccess::Write, .clear_before_use = true}, + }, + .execute = [](const render::rendering::RenderPassExecutionContext&) {}, + .enabled = [bloom](const render::rendering::RenderPassExecutionContext&) { return bloom.enabled; }, + }, &graph_error); + + pass_registry.add_pass(render::rendering::RenderPassDefinition{ + .name = "bloom-composite-pass", + .depends_on = {"main-lit-pass", "bloom-blur-pass"}, + .resources = { + {.resource = "scene_color", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "bloom_blur", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "post_color", .access = render::rendering::RenderResourceAccess::Write, .clear_before_use = true}, + }, + .execute = [](const render::rendering::RenderPassExecutionContext&) {}, + .enabled = {}, + }, &graph_error); + + pass_registry.add_pass(render::rendering::RenderPassDefinition{ + .name = "outline-pass", + .depends_on = {"main-lit-pass"}, + .resources = { + {.resource = "scene_color", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "outline_mask", .access = render::rendering::RenderResourceAccess::Write, .clear_before_use = true}, + }, + .execute = [](const render::rendering::RenderPassExecutionContext&) {}, + .enabled = {}, + }, &graph_error); + + pass_registry.add_pass(render::rendering::RenderPassDefinition{ + .name = "ui-pass", + .depends_on = {"bloom-composite-pass", "outline-pass"}, + .resources = { + {.resource = "post_color", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "ui_overlay", .access = render::rendering::RenderResourceAccess::Write, .clear_before_use = true}, + }, + .execute = [](const render::rendering::RenderPassExecutionContext&) {}, + .enabled = {}, + }, &graph_error); + + pass_registry.add_pass(render::rendering::RenderPassDefinition{ + .name = "debug-pass", + .depends_on = {"ui-pass"}, + .resources = { + {.resource = "post_color", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "outline_mask", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "ui_overlay", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "debug_overlay", .access = render::rendering::RenderResourceAccess::Write, .clear_before_use = true}, + }, + .execute = [&](const render::rendering::RenderPassExecutionContext&) { + renderer.set_debug_counters(render::rendering::RendererDebugCounters{ + .visible_directional_lights = static_cast(frame_state.directional_lights.size()), + .visible_point_lights = static_cast(frame_state.point_lights.size()), + .submitted_draws = frame_state.submission_diagnostics.submitted_draws, + .instanced_draws = frame_state.submission_diagnostics.instanced_draws, + .submitted_instances = frame_state.submission_diagnostics.submitted_instances, + .vfx_active_effects = frame_state.vfx_diagnostics.total_active_effects, + .vfx_active_particles = frame_state.vfx_diagnostics.total_active_particles, + .vfx_draw_calls = frame_state.vfx_diagnostics.draw_calls, + .vfx_instance_uploads = frame_state.vfx_diagnostics.uploaded_instances, + }); + }, + .enabled = {}, + }, &graph_error); + + pass_registry.add_pass(render::rendering::RenderPassDefinition{ + .name = "present-pass", + .depends_on = {"debug-pass"}, + .resources = { + {.resource = "post_color", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "outline_mask", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "ui_overlay", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "debug_overlay", .access = render::rendering::RenderResourceAccess::Read}, + {.resource = "backbuffer", .access = render::rendering::RenderResourceAccess::Write}, + }, + .execute = [](const render::rendering::RenderPassExecutionContext&) {}, + .enabled = {}, + }, &graph_error); + + if (const auto build_result = pass_registry.build(); !build_result.ok) { + render::platform::log::error(std::string{"Render pass build failed: "} + build_result.error); + renderer.end_frame(); + runtime.end_frame(); + continue; + } + + if (!logged_graph) { + render::platform::log::info(pass_registry.dump_graph()); + logged_graph = true; } - const auto vfx_pass_start = std::chrono::steady_clock::now(); - auto vfx_submissions = vfx_system.build_frame_submissions(kMainView); - for (const auto& vfx : vfx_submissions) { - renderer.submit_instanced(kMainView, vfx.draw, std::span{vfx.transforms}); + + render::rendering::RenderPassExecutionContext pass_context{}; + pass_context.frame = render::rendering::RenderFrameInfo{ + .frame_index = renderer.status().frame.frame_count, + .backbuffer_width = static_cast(window.width), + .backbuffer_height = static_cast(window.height), + .delta_seconds = 1.0F / 60.0F, + }; + pass_context.renderer = &renderer; + pass_context.registry = &pass_registry; + pass_context.user_data = &frame_state; + + if (!pass_registry.execute(pass_context, &graph_error)) { + render::platform::log::error(std::string{"Render pass execute failed: "} + graph_error); } - const auto vfx_pass_end = std::chrono::steady_clock::now(); - - const auto main_pass_end = std::chrono::steady_clock::now(); - renderer.add_debug_pass_timing(render::rendering::RendererPassTiming{ - .pass_name = "main-lit-pass", - .cpu_ms = static_cast(std::chrono::duration(main_pass_end - main_pass_start).count()), - .gpu_ms = std::nullopt, - }); - renderer.add_debug_pass_timing(render::rendering::RendererPassTiming{ - .pass_name = "vfx-ambient-pass", - .cpu_ms = static_cast(std::chrono::duration(vfx_pass_end - vfx_pass_start).count()), - .gpu_ms = std::nullopt, - }); - renderer.add_debug_pass_timing(render::rendering::RendererPassTiming{.pass_name = "bloom-pass(plan)", .cpu_ms = 0.0F, .gpu_ms = std::nullopt}); - renderer.add_debug_pass_timing(render::rendering::RendererPassTiming{.pass_name = "outline-pass(plan)", .cpu_ms = 0.0F, .gpu_ms = std::nullopt}); - renderer.add_debug_pass_timing(render::rendering::RendererPassTiming{.pass_name = "composite-present", .cpu_ms = 0.0F, .gpu_ms = std::nullopt}); - renderer.set_debug_counters(render::rendering::RendererDebugCounters{ - .visible_directional_lights = static_cast(directional_lights.size()), - .visible_point_lights = static_cast(point_lights.size()), - .submitted_draws = diagnostics.submitted_draws, - .instanced_draws = diagnostics.instanced_draws, - .submitted_instances = diagnostics.submitted_instances, - .vfx_active_effects = vfx_system.diagnostics().total_active_effects, - .vfx_active_particles = vfx_system.diagnostics().total_active_particles, - .vfx_draw_calls = vfx_system.diagnostics().draw_calls, - .vfx_instance_uploads = vfx_system.diagnostics().uploaded_instances, - }); renderer.end_frame(); runtime.end_frame(); diff --git a/tests/render/render_pass_system_tests.cpp b/tests/render/render_pass_system_tests.cpp new file mode 100644 index 0000000..a79871b --- /dev/null +++ b/tests/render/render_pass_system_tests.cpp @@ -0,0 +1,104 @@ +#include "engine/render/render_pass_system.hpp" + +#include +#include +#include +#include + +int main() { + using namespace render::rendering; + + RenderPassRegistry registry; + std::string error; + + assert(registry.declare_resource({.name = "shadow", .kind = RenderResourceKind::ShadowMap}, &error)); + assert(registry.declare_resource({.name = "scene", .kind = RenderResourceKind::RenderTarget}, &error)); + assert(registry.declare_resource({.name = "post", .kind = RenderResourceKind::PostProcessTexture}, &error)); + assert(registry.declare_resource({.name = "backbuffer", .kind = RenderResourceKind::Backbuffer, .imported = true, .transient = false}, &error)); + + std::vector executed; + + assert(registry.add_pass(RenderPassDefinition{ + .name = "shadow-pass", + .resources = {{.resource = "shadow", .access = RenderResourceAccess::Write}}, + .execute = [&](const RenderPassExecutionContext&) { executed.push_back("shadow"); }, + }, &error)); + + assert(registry.add_pass(RenderPassDefinition{ + .name = "main-pass", + .depends_on = {"shadow-pass"}, + .resources = { + {.resource = "shadow", .access = RenderResourceAccess::Read}, + {.resource = "scene", .access = RenderResourceAccess::Write}, + }, + .execute = [&](const RenderPassExecutionContext&) { executed.push_back("main"); }, + }, &error)); + + bool post_enabled = false; + assert(registry.add_pass(RenderPassDefinition{ + .name = "post-pass", + .depends_on = {"main-pass"}, + .resources = { + {.resource = "scene", .access = RenderResourceAccess::Read}, + {.resource = "post", .access = RenderResourceAccess::Write}, + }, + .execute = [&](const RenderPassExecutionContext&) { executed.push_back("post"); }, + .enabled = [&](const RenderPassExecutionContext&) { return post_enabled; }, + }, &error)); + + assert(registry.add_pass(RenderPassDefinition{ + .name = "present-pass", + .depends_on = {"post-pass"}, + .resources = { + {.resource = "post", .access = RenderResourceAccess::Read}, + {.resource = "backbuffer", .access = RenderResourceAccess::Write}, + }, + .execute = [&](const RenderPassExecutionContext&) { executed.push_back("present"); }, + }, &error)); + + const auto build = registry.build(); + assert(build.ok); + assert(registry.ordered_passes().size() == 4); + + RenderPassExecutionContext context{}; + context.frame.frame_index = 4; + assert(registry.execute(context, &error)); + assert(executed.size() == 3); + assert(executed[0] == "shadow"); + assert(executed[1] == "main"); + assert(executed[2] == "present"); + + const auto diagnostics = registry.diagnostics(); + assert(diagnostics.inactive_passes.size() == 1); + assert(diagnostics.inactive_passes[0] == "post-pass"); + + RenderPassRegistry invalid; + assert(invalid.declare_resource({.name = "scene", .kind = RenderResourceKind::RenderTarget}, &error)); + assert(invalid.add_pass(RenderPassDefinition{ + .name = "present", + .resources = {{.resource = "scene", .access = RenderResourceAccess::Read}}, + .execute = [](const RenderPassExecutionContext&) {}, + }, &error)); + const auto invalid_build = invalid.build(); + assert(!invalid_build.ok); + + RenderPassRegistry cycle; + assert(cycle.declare_resource({.name = "a", .kind = RenderResourceKind::RenderTarget}, &error)); + assert(cycle.declare_resource({.name = "b", .kind = RenderResourceKind::RenderTarget}, &error)); + assert(cycle.add_pass(RenderPassDefinition{ + .name = "A", + .depends_on = {"B"}, + .resources = {{.resource = "a", .access = RenderResourceAccess::Write}}, + .execute = [](const RenderPassExecutionContext&) {}, + }, &error)); + assert(cycle.add_pass(RenderPassDefinition{ + .name = "B", + .depends_on = {"A"}, + .resources = {{.resource = "b", .access = RenderResourceAccess::Write}}, + .execute = [](const RenderPassExecutionContext&) {}, + }, &error)); + const auto cycle_build = cycle.build(); + assert(!cycle_build.ok); + + return 0; +}