From ed0ff3be39bda29cee86dc2d637b4d6f54ce8c77 Mon Sep 17 00:00:00 2001 From: ercmine Date: Sun, 5 Apr 2026 20:32:54 -0500 Subject: [PATCH] Add renderer debug modes, overlays, and timing scaffolding --- CMakeLists.txt | 11 +- docs/rendering.md | 50 +++++++ engine/render/README.md | 27 ++++ engine/render/bgfx/renderer_bgfx.cpp | 118 ++++++++++++++- engine/render/debug_renderer.cpp | 151 ++++++++++++++++++++ engine/render/debug_renderer.hpp | 88 ++++++++++++ engine/render/renderer.hpp | 9 ++ engine/shell/main.cpp | 73 +++++++++- shaders/README.md | 7 + shaders/debug/renderer_debug/fs_albedo.sc | 8 ++ shaders/debug/renderer_debug/fs_depth.sc | 9 ++ shaders/debug/renderer_debug/fs_normals.sc | 9 ++ shaders/debug/renderer_debug/fs_overdraw.sc | 8 ++ shaders/debug/renderer_debug/vs.sc | 12 ++ shaders/includes/varying_color.def.sc | 1 + shaders/shaders.cmake | 4 + tests/render/debug_renderer_tests.cpp | 44 ++++++ tests/render/renderer_lifecycle_tests.cpp | 4 + 18 files changed, 625 insertions(+), 8 deletions(-) create mode 100644 engine/render/debug_renderer.cpp create mode 100644 engine/render/debug_renderer.hpp create mode 100644 shaders/debug/renderer_debug/fs_albedo.sc create mode 100644 shaders/debug/renderer_debug/fs_depth.sc create mode 100644 shaders/debug/renderer_debug/fs_normals.sc create mode 100644 shaders/debug/renderer_debug/fs_overdraw.sc create mode 100644 shaders/debug/renderer_debug/vs.sc create mode 100644 tests/render/debug_renderer_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b19016c..56ce5a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,6 +114,8 @@ add_library(render_renderer STATIC engine/render/renderer_types.hpp engine/render/buffer_types.hpp engine/render/buffer_types.cpp + engine/render/debug_renderer.hpp + engine/render/debug_renderer.cpp engine/render/draw_submission.hpp engine/render/draw_submission.cpp engine/render/shader_types.hpp @@ -219,6 +221,11 @@ if(RENDER_BUILD_TESTS) render_apply_project_options(render_lighting_pipeline_tests) render_apply_warnings(render_lighting_pipeline_tests) + add_executable(render_debug_renderer_tests tests/render/debug_renderer_tests.cpp) + target_link_libraries(render_debug_renderer_tests PRIVATE render::renderer) + render_apply_project_options(render_debug_renderer_tests) + render_apply_warnings(render_debug_renderer_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) @@ -240,6 +247,8 @@ if(RENDER_BUILD_TESTS) set_tests_properties(unit.renderer.geometry_submission PROPERTIES LABELS "unit;renderer;geometry") add_test(NAME unit.renderer.lighting_pipeline COMMAND render_lighting_pipeline_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.scene.runtime COMMAND render_scene_tests) set_tests_properties(unit.scene.runtime PROPERTIES LABELS "unit;scene") add_test(NAME headless.smoke.startup COMMAND render_headless_validation) @@ -247,7 +256,7 @@ if(RENDER_BUILD_TESTS) add_custom_target(render_test_unit COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure --label-regex unit - DEPENDS render_platform_types_tests render_core_runtime_tests render_serialization_tests render_filesystem_tests render_renderer_lifecycle_tests render_shader_pipeline_tests render_geometry_submission_tests render_lighting_pipeline_tests render_scene_tests + DEPENDS render_platform_types_tests render_core_runtime_tests render_serialization_tests render_filesystem_tests render_renderer_lifecycle_tests render_shader_pipeline_tests render_geometry_submission_tests render_lighting_pipeline_tests render_debug_renderer_tests render_scene_tests WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" COMMENT "Running render unit tests" ) diff --git a/docs/rendering.md b/docs/rendering.md index baf4e0e..595eddf 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -201,3 +201,53 @@ This avoids unbounded \"every object loops every light\" behavior while preservi - Bloom extraction/blur/composite and outline composition passes. - Full lit material shader programs (current shell still renders via debug triangle program). - Debug view rendering modes (normals, emissive-only, bloom extraction, shadow atlas visualization). + +## Renderer debug views (Statement 14) + +The renderer now includes an engine-owned debug layer (`engine/render/debug_renderer.*`) instead of shell-only ad hoc toggles. + +### Modes + +- Disabled (normal renderer path) +- Wireframe +- Normals +- Albedo +- Depth +- Light volumes +- Overdraw approximation +- GPU timing view + +### Runtime controls (shell) + +- `1` Disabled +- `2` Wireframe +- `3` Normals +- `4` Albedo +- `5` Depth +- `6` Light volumes +- `7` Overdraw approximation +- `8` GPU timing view +- `Tab` cycle modes + +Mode switching is routed through `Renderer` debug APIs and logged by renderer code. + +### Mode interpretation + +- Normals: uses a debug shader that maps normalized direction to RGB (`normal * 0.5 + 0.5`). +- Albedo: displays base vertex/material color contribution (lighting bypassed). +- Depth: linearized remap of fragment depth to grayscale for clipping/layer checks. +- Light volumes: keeps normal rendering while exposing light-population diagnostics in overlay text. +- Overdraw: additive no-depth accumulation approximation; brighter regions imply more overlapping shaded fragments. + +### GPU timing view and current limits + +`RendererPassTiming` supports GPU and CPU data, but current statement implementation records CPU pass durations and marks GPU values unavailable (`gpu=n/a`) when backend timing hooks are not wired yet. + +Tracked pass rows currently include: + +- shadow pass planning +- main lit submission +- bloom/outline placeholders +- composite/present placeholder + +This keeps the instrumentation API stable while deferring backend-specific GPU timer query plumbing to a follow-up statement. diff --git a/engine/render/README.md b/engine/render/README.md index 889ed8f..6486e3c 100644 --- a/engine/render/README.md +++ b/engine/render/README.md @@ -49,3 +49,30 @@ bgfx handles remain internal to backend implementation (`engine/render/bgfx/rend - Diagnostics: selected/culled light counts and highlighted/shadowed object metrics. This module intentionally keeps bgfx API details out of gameplay-facing code while preparing a scalable path to tiled/clustered improvements later. + +## Renderer debug architecture (Statement 14) + +`debug_renderer.hpp/.cpp` now owns renderer debug state and overlay formatting: + +- canonical mode enum (`RendererDebugMode`) with runtime switching helpers +- frame counters (lights/draws/instances) +- pass timing samples (GPU optional, CPU fallback) +- overlay/HUD line generation for runtime shells and future tools +- program overrides used to translate regular scene submissions into debug views + +Renderer-facing API hooks are exposed directly on `Renderer`: + +- `set_debug_mode`, `set_debug_mode_from_index`, `cycle_debug_mode` +- `set_debug_program_overrides` +- `set_debug_counters`, `add_debug_pass_timing` +- `debug_snapshot` for tool/runtime inspection + +Current debug view behavior: + +- `wireframe`: backend wireframe flag wrapped by engine mode. +- `normals`: debug program override visualizing normalized direction. +- `albedo`: debug program override showing base vertex color. +- `depth`: debug program override showing remapped depth. +- `light-volumes`: normal shading with overlay diagnostics for light counts. +- `overdraw`: additive/no-depth approximation pass override. +- `gpu-timing`: overlay emphasizes per-pass timing rows; current implementation is CPU-timed with explicit `gpu=n/a` fallback. diff --git a/engine/render/bgfx/renderer_bgfx.cpp b/engine/render/bgfx/renderer_bgfx.cpp index e58d159..a2c8b44 100644 --- a/engine/render/bgfx/renderer_bgfx.cpp +++ b/engine/render/bgfx/renderer_bgfx.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -196,6 +197,7 @@ struct Renderer::Impl { std::vector bytes{}; }; std::unordered_map instance_buffers{}; + RendererDebugState debug_state{}; [[nodiscard]] bool is_initialized() const noexcept { return bgfx_initialized; @@ -303,6 +305,7 @@ bool Renderer::initialize(const RendererConfig& config, const platform::Platform impl_->device_lost = false; impl_->frame_active = false; impl_->has_pending_resize = false; + impl_->debug_state = RendererDebugState{}; bgfx::Init init{}; init.type = to_bgfx_renderer_type(config.backend); @@ -336,6 +339,7 @@ bool Renderer::initialize(const RendererConfig& config, const platform::Platform impl_->selected_backend = to_backend(bgfx::getRendererType()); impl_->backbuffer_width = config.width; impl_->backbuffer_height = config.height; + impl_->debug_state.set_gpu_timing_supported(false); const std::uint32_t debug_flags = config.debug ? BGFX_DEBUG_TEXT : BGFX_DEBUG_NONE; bgfx::setDebug(debug_flags); @@ -458,9 +462,25 @@ bool Renderer::begin_frame() { impl_->frame_active = true; impl_->frame_begin_time = std::chrono::steady_clock::now(); + RendererDebugSnapshot baseline{}; + baseline.mode = impl_->debug_state.mode(); + baseline.backend = impl_->selected_backend; + baseline.backbuffer_width = impl_->backbuffer_width; + baseline.backbuffer_height = impl_->backbuffer_height; + baseline.frame_time_ms = impl_->last_frame_time_ms; + impl_->debug_state.begin_frame(baseline); + if (impl_->config.debug) { bgfx::dbgTextClear(); - bgfx::dbgTextPrintf(0, 0, 0x0f, "render :: backend %s", renderer_name(bgfx::getRendererType())); + const std::uint32_t debug_flags = + impl_->debug_state.mode() == RendererDebugMode::Wireframe ? BGFX_DEBUG_TEXT | BGFX_DEBUG_WIREFRAME : BGFX_DEBUG_TEXT; + bgfx::setDebug(debug_flags); + const auto overlay_lines = impl_->debug_state.build_overlay_lines(); + for (std::size_t i = 0; i < overlay_lines.size(); ++i) { + bgfx::dbgTextPrintf(0, static_cast(i), 0x0f, "%s", overlay_lines[i].c_str()); + } + } else { + bgfx::setDebug(BGFX_DEBUG_NONE); } return true; @@ -534,8 +554,62 @@ void Renderer::resize(const std::uint32_t width, const std::uint32_t height) { void Renderer::set_debug_enabled(const bool enabled) { impl_->config.debug = enabled; if (impl_->lifecycle == RendererLifecycleState::Ready) { - bgfx::setDebug(enabled ? BGFX_DEBUG_TEXT : BGFX_DEBUG_NONE); + const std::uint32_t debug_flags = + (enabled && impl_->debug_state.mode() == RendererDebugMode::Wireframe) ? BGFX_DEBUG_TEXT | BGFX_DEBUG_WIREFRAME + : (enabled ? BGFX_DEBUG_TEXT : BGFX_DEBUG_NONE); + bgfx::setDebug(debug_flags); + } +} + +bool Renderer::set_debug_mode(const RendererDebugMode mode) { + if (!impl_->debug_state.set_mode(mode)) { + return false; } + std::ostringstream stream; + stream << "Renderer debug mode switched to " << to_string(mode); + platform::log::info(stream.str()); + return true; +} + +RendererDebugMode Renderer::debug_mode() const noexcept { + return impl_->debug_state.mode(); +} + +bool Renderer::set_debug_mode_from_index(const std::uint8_t index) { + const std::optional mode = renderer_debug_mode_from_index(index); + if (!mode.has_value()) { + std::ostringstream stream; + stream << "Renderer debug mode index out of range: " << static_cast(index); + platform::log::warn(stream.str()); + return false; + } + return set_debug_mode(*mode); +} + +bool Renderer::cycle_debug_mode() { + const bool changed = impl_->debug_state.cycle_next_mode(); + if (changed) { + std::ostringstream stream; + stream << "Renderer debug mode switched to " << to_string(impl_->debug_state.mode()); + platform::log::info(stream.str()); + } + return changed; +} + +void Renderer::set_debug_program_overrides(const RendererDebugProgramOverrides& overrides) { + impl_->debug_state.set_program_overrides(overrides); +} + +void Renderer::set_debug_counters(const RendererDebugCounters& counters) { + impl_->debug_state.set_counters(counters); +} + +void Renderer::add_debug_pass_timing(const RendererPassTiming& timing) { + impl_->debug_state.add_pass_timing(timing); +} + +RendererDebugSnapshot Renderer::debug_snapshot() const { + return impl_->debug_state.snapshot(); } void Renderer::set_view(const ViewId view, const ViewDescription& desc) { @@ -758,11 +832,27 @@ void Renderer::submit(const ViewId view, const DrawSubmission& mesh_submission) bgfx::setIndexBuffer(bgfx::IndexBufferHandle{mesh_submission.mesh.index_buffer.idx}, 0, mesh_submission.mesh.index_count); } - const std::uint64_t state = mesh_submission.draw_state.state_flags != 0 + const RendererDebugMode debug_mode = impl_->debug_state.mode(); + const RendererDebugProgramOverrides overrides = impl_->debug_state.program_overrides(); + ProgramHandle program = mesh_submission.material.program; + if (debug_mode == RendererDebugMode::Normals && overrides.normals.idx != kInvalidHandle) { + program = overrides.normals; + } else if (debug_mode == RendererDebugMode::Albedo && overrides.albedo.idx != kInvalidHandle) { + program = overrides.albedo; + } else if (debug_mode == RendererDebugMode::Depth && overrides.depth.idx != kInvalidHandle) { + program = overrides.depth; + } else if (debug_mode == RendererDebugMode::Overdraw && overrides.overdraw.idx != kInvalidHandle) { + program = overrides.overdraw; + } + + std::uint64_t state = mesh_submission.draw_state.state_flags != 0 ? mesh_submission.draw_state.state_flags : static_cast(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_WRITE_Z | BGFX_STATE_DEPTH_TEST_LESS); + if (debug_mode == RendererDebugMode::Overdraw) { + state = BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_BLEND_ADD | BGFX_STATE_DEPTH_TEST_ALWAYS; + } bgfx::setState(state); - bgfx::submit(view.value, bgfx::ProgramHandle{mesh_submission.material.program.idx}); + bgfx::submit(view.value, bgfx::ProgramHandle{program.idx}); } void Renderer::submit_instanced(const ViewId view, const DrawSubmission& submission, const std::span transforms) { @@ -783,11 +873,27 @@ void Renderer::submit_instanced(const ViewId view, const DrawSubmission& submiss bgfx::setIndexBuffer(bgfx::IndexBufferHandle{submission.mesh.index_buffer.idx}, 0, submission.mesh.index_count); } bgfx::setInstanceDataBuffer(&idb); - const std::uint64_t state = submission.draw_state.state_flags != 0 + const RendererDebugMode debug_mode = impl_->debug_state.mode(); + const RendererDebugProgramOverrides overrides = impl_->debug_state.program_overrides(); + ProgramHandle program = submission.material.program; + if (debug_mode == RendererDebugMode::Normals && overrides.normals.idx != kInvalidHandle) { + program = overrides.normals; + } else if (debug_mode == RendererDebugMode::Albedo && overrides.albedo.idx != kInvalidHandle) { + program = overrides.albedo; + } else if (debug_mode == RendererDebugMode::Depth && overrides.depth.idx != kInvalidHandle) { + program = overrides.depth; + } else if (debug_mode == RendererDebugMode::Overdraw && overrides.overdraw.idx != kInvalidHandle) { + program = overrides.overdraw; + } + + std::uint64_t state = submission.draw_state.state_flags != 0 ? submission.draw_state.state_flags : static_cast(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_WRITE_Z | BGFX_STATE_DEPTH_TEST_LESS); + if (debug_mode == RendererDebugMode::Overdraw) { + state = BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_BLEND_ADD | BGFX_STATE_DEPTH_TEST_ALWAYS; + } bgfx::setState(state); - bgfx::submit(view.value, bgfx::ProgramHandle{submission.material.program.idx}); + bgfx::submit(view.value, bgfx::ProgramHandle{program.idx}); } } // namespace render::rendering diff --git a/engine/render/debug_renderer.cpp b/engine/render/debug_renderer.cpp new file mode 100644 index 0000000..32404d1 --- /dev/null +++ b/engine/render/debug_renderer.cpp @@ -0,0 +1,151 @@ +#include "engine/render/debug_renderer.hpp" + +#include + +namespace render::rendering { +namespace { + +const char* backend_label(const RendererBackend backend) noexcept { + switch (backend) { + case RendererBackend::Auto: return "auto"; + case RendererBackend::Noop: return "noop"; + case RendererBackend::Direct3D11: return "d3d11"; + case RendererBackend::Direct3D12: return "d3d12"; + case RendererBackend::Metal: return "metal"; + case RendererBackend::Vulkan: return "vulkan"; + case RendererBackend::OpenGL: return "opengl"; + default: return "unknown"; + } +} + +} // namespace + +const char* to_string(const RendererDebugMode mode) noexcept { + switch (mode) { + case RendererDebugMode::Disabled: return "disabled"; + case RendererDebugMode::Wireframe: return "wireframe"; + case RendererDebugMode::Normals: return "normals"; + case RendererDebugMode::Albedo: return "albedo"; + case RendererDebugMode::Depth: return "depth"; + case RendererDebugMode::LightVolumes: return "light-volumes"; + case RendererDebugMode::Overdraw: return "overdraw"; + case RendererDebugMode::GpuTiming: return "gpu-timing"; + default: return "unknown"; + } +} + +std::optional renderer_debug_mode_from_index(const std::uint8_t index) noexcept { + if (index >= kRendererDebugModeCount) { + return std::nullopt; + } + return static_cast(index); +} + +RendererDebugMode RendererDebugState::mode() const noexcept { + return mode_; +} + +bool RendererDebugState::set_mode(const RendererDebugMode mode) { + if (mode_ == mode) { + return true; + } + mode_ = mode; + snapshot_.mode = mode_; + return true; +} + +bool RendererDebugState::set_mode_from_index(const std::uint8_t index) { + const std::optional mode = renderer_debug_mode_from_index(index); + if (!mode.has_value()) { + return false; + } + return set_mode(*mode); +} + +bool RendererDebugState::cycle_next_mode() { + const auto next = static_cast((static_cast(mode_) + 1U) % kRendererDebugModeCount); + return set_mode(static_cast(next)); +} + +void RendererDebugState::set_gpu_timing_supported(const bool supported) noexcept { + gpu_timing_supported_ = supported; + snapshot_.gpu_timing_supported = supported; +} + +bool RendererDebugState::gpu_timing_supported() const noexcept { + return gpu_timing_supported_; +} + +void RendererDebugState::set_program_overrides(const RendererDebugProgramOverrides& overrides) noexcept { + program_overrides_ = overrides; +} + +const RendererDebugProgramOverrides& RendererDebugState::program_overrides() const noexcept { + return program_overrides_; +} + +void RendererDebugState::begin_frame(const RendererDebugSnapshot& baseline) { + snapshot_ = baseline; + snapshot_.mode = mode_; + snapshot_.gpu_timing_supported = gpu_timing_supported_; + snapshot_.timings.clear(); +} + +void RendererDebugState::set_counters(const RendererDebugCounters& counters) { + snapshot_.counters = counters; +} + +void RendererDebugState::add_pass_timing(const RendererPassTiming& timing) { + snapshot_.timings.push_back(timing); +} + +RendererDebugSnapshot RendererDebugState::snapshot() const { + return snapshot_; +} + +std::vector RendererDebugState::build_overlay_lines() const { + std::vector lines; + lines.reserve(10U + snapshot_.timings.size()); + + std::ostringstream header; + header << "render debug mode=" << to_string(snapshot_.mode) + << " backend=" << backend_label(snapshot_.backend) + << " backbuffer=" << snapshot_.backbuffer_width << "x" << snapshot_.backbuffer_height; + lines.push_back(header.str()); + + std::ostringstream counts; + counts << "lights dir=" << snapshot_.counters.visible_directional_lights + << " point=" << snapshot_.counters.visible_point_lights + << " draws=" << snapshot_.counters.submitted_draws + << " instanced=" << snapshot_.counters.instanced_draws + << " instances=" << snapshot_.counters.submitted_instances + << " frame_ms=" << snapshot_.frame_time_ms; + lines.push_back(counts.str()); + + if (!snapshot_.gpu_timing_supported) { + lines.push_back("gpu timing unavailable (using cpu pass timings)"); + } + + for (const RendererPassTiming& timing : snapshot_.timings) { + std::ostringstream pass; + pass << timing.pass_name << " cpu=" << timing.cpu_ms << "ms"; + if (timing.gpu_ms.has_value()) { + pass << " gpu=" << *timing.gpu_ms << "ms"; + } else { + pass << " gpu=n/a"; + } + lines.push_back(pass.str()); + } + + if (snapshot_.mode == RendererDebugMode::LightVolumes) { + lines.push_back("light-volumes mode overlays per-light influence diagnostics"); + } else if (snapshot_.mode == RendererDebugMode::Depth) { + lines.push_back("depth mode uses linearized near/far remap in debug shader"); + } else if (snapshot_.mode == RendererDebugMode::Overdraw) { + lines.push_back("overdraw mode approximates cost via additive no-depth accumulation"); + } + + return lines; +} + +} // namespace render::rendering diff --git a/engine/render/debug_renderer.hpp b/engine/render/debug_renderer.hpp new file mode 100644 index 0000000..d6d331a --- /dev/null +++ b/engine/render/debug_renderer.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include "engine/render/renderer_types.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace render::rendering { + +enum class RendererDebugMode : std::uint8_t { + Disabled = 0, + Wireframe, + Normals, + Albedo, + Depth, + LightVolumes, + Overdraw, + GpuTiming, +}; + +constexpr std::size_t kRendererDebugModeCount = 8; + +struct RendererDebugCounters { + std::uint32_t visible_directional_lights{0}; + std::uint32_t visible_point_lights{0}; + std::uint32_t submitted_draws{0}; + std::uint32_t instanced_draws{0}; + std::uint32_t submitted_instances{0}; +}; + +struct RendererPassTiming { + std::string pass_name{}; + float cpu_ms{0.0F}; + std::optional gpu_ms{}; +}; + +struct RendererDebugSnapshot { + RendererDebugMode mode{RendererDebugMode::Disabled}; + RendererBackend backend{RendererBackend::Auto}; + std::uint32_t backbuffer_width{0}; + std::uint32_t backbuffer_height{0}; + std::uint32_t frame_time_ms{0}; + RendererDebugCounters counters{}; + bool gpu_timing_supported{false}; + std::vector timings{}; +}; + +struct RendererDebugProgramOverrides { + ProgramHandle normals{}; + ProgramHandle albedo{}; + ProgramHandle depth{}; + ProgramHandle overdraw{}; +}; + +[[nodiscard]] const char* to_string(RendererDebugMode mode) noexcept; +[[nodiscard]] std::optional renderer_debug_mode_from_index(std::uint8_t index) noexcept; + +class RendererDebugState { +public: + [[nodiscard]] RendererDebugMode mode() const noexcept; + [[nodiscard]] bool set_mode(RendererDebugMode mode); + [[nodiscard]] bool set_mode_from_index(std::uint8_t index); + [[nodiscard]] bool cycle_next_mode(); + + void set_gpu_timing_supported(bool supported) noexcept; + [[nodiscard]] bool gpu_timing_supported() const noexcept; + void set_program_overrides(const RendererDebugProgramOverrides& overrides) noexcept; + [[nodiscard]] const RendererDebugProgramOverrides& program_overrides() const noexcept; + + void begin_frame(const RendererDebugSnapshot& baseline); + void set_counters(const RendererDebugCounters& counters); + void add_pass_timing(const RendererPassTiming& timing); + [[nodiscard]] RendererDebugSnapshot snapshot() const; + [[nodiscard]] std::vector build_overlay_lines() const; + +private: + RendererDebugMode mode_{RendererDebugMode::Disabled}; + bool gpu_timing_supported_{false}; + RendererDebugProgramOverrides program_overrides_{}; + RendererDebugSnapshot snapshot_{}; +}; + +} // namespace render::rendering diff --git a/engine/render/renderer.hpp b/engine/render/renderer.hpp index 469a04c..a2c4b34 100644 --- a/engine/render/renderer.hpp +++ b/engine/render/renderer.hpp @@ -1,6 +1,7 @@ #pragma once #include "engine/render/buffer_types.hpp" +#include "engine/render/debug_renderer.hpp" #include "engine/render/draw_submission.hpp" #include "engine/render/renderer_types.hpp" #include "engine/render/shader_types.hpp" @@ -40,6 +41,14 @@ class Renderer { void resize(std::uint32_t width, std::uint32_t height); void set_debug_enabled(bool enabled); + [[nodiscard]] bool set_debug_mode(RendererDebugMode mode); + [[nodiscard]] RendererDebugMode debug_mode() const noexcept; + [[nodiscard]] bool set_debug_mode_from_index(std::uint8_t index); + [[nodiscard]] bool cycle_debug_mode(); + void set_debug_program_overrides(const RendererDebugProgramOverrides& overrides); + void set_debug_counters(const RendererDebugCounters& counters); + void add_debug_pass_timing(const RendererPassTiming& timing); + [[nodiscard]] RendererDebugSnapshot debug_snapshot() const; void set_view(ViewId view, const ViewDescription& desc); void set_view_transform(ViewId view, std::span view_transform, std::span projection); diff --git a/engine/shell/main.cpp b/engine/shell/main.cpp index 4ddd579..2840d0a 100644 --- a/engine/shell/main.cpp +++ b/engine/shell/main.cpp @@ -8,8 +8,10 @@ #include "engine/scene/scene.hpp" #include +#include #include #include +#include #include #include #include @@ -39,6 +41,32 @@ render::rendering::RendererBackend parse_renderer_backend_from_args(const int ar std::array to_array(const render::core::Mat4& matrix) { return matrix.m; } +bool key_pressed(const render::platform::InputFrame& input, const render::platform::Key key) { + return input.keyboard.keys[render::platform::to_index(key)].pressed; +} + +void apply_debug_mode_hotkeys(const render::platform::InputFrame& input, render::rendering::Renderer& renderer) { + if (key_pressed(input, render::platform::Key::Num1)) { + renderer.set_debug_mode(render::rendering::RendererDebugMode::Disabled); + } else if (key_pressed(input, render::platform::Key::Num2)) { + renderer.set_debug_mode(render::rendering::RendererDebugMode::Wireframe); + } else if (key_pressed(input, render::platform::Key::Num3)) { + renderer.set_debug_mode(render::rendering::RendererDebugMode::Normals); + } else if (key_pressed(input, render::platform::Key::Num4)) { + renderer.set_debug_mode(render::rendering::RendererDebugMode::Albedo); + } else if (key_pressed(input, render::platform::Key::Num5)) { + renderer.set_debug_mode(render::rendering::RendererDebugMode::Depth); + } else if (key_pressed(input, render::platform::Key::Num6)) { + renderer.set_debug_mode(render::rendering::RendererDebugMode::LightVolumes); + } else if (key_pressed(input, render::platform::Key::Num7)) { + renderer.set_debug_mode(render::rendering::RendererDebugMode::Overdraw); + } else if (key_pressed(input, render::platform::Key::Num8)) { + renderer.set_debug_mode(render::rendering::RendererDebugMode::GpuTiming); + } else if (key_pressed(input, render::platform::Key::Tab)) { + renderer.cycle_debug_mode(); + } +} + } // namespace int main(int argc, char** argv) { @@ -46,7 +74,7 @@ int main(int argc, char** argv) { render::platform::RuntimeConfig platform_config{}; platform_config.app_name = "render-shell"; platform_config.org_name = "render"; - platform_config.window.title = "render :: Statement 13 stylized lighting shell"; + platform_config.window.title = "render :: Statement 14 renderer debug shell"; platform_config.window.width = 1280; platform_config.window.height = 720; platform_config.window.resizable = true; @@ -94,6 +122,20 @@ int main(int argc, char** argv) { render::rendering::ShaderProgramLibrary shader_library{renderer, filesystem}; const render::rendering::ShaderProgramId shader_id{.category = "debug", .name = "debug_triangle", .variant = "default"}; render::rendering::ProgramHandle program = shader_library.load_program(shader_id); + const render::rendering::ProgramHandle debug_normals = shader_library.load_program( + render::rendering::ShaderProgramId{.category = "debug", .name = "renderer_debug_normals", .variant = "default"}); + const render::rendering::ProgramHandle debug_albedo = shader_library.load_program( + render::rendering::ShaderProgramId{.category = "debug", .name = "renderer_debug_albedo", .variant = "default"}); + const render::rendering::ProgramHandle debug_depth = shader_library.load_program( + render::rendering::ShaderProgramId{.category = "debug", .name = "renderer_debug_depth", .variant = "default"}); + const render::rendering::ProgramHandle debug_overdraw = shader_library.load_program( + render::rendering::ShaderProgramId{.category = "debug", .name = "renderer_debug_overdraw", .variant = "default"}); + renderer.set_debug_program_overrides(render::rendering::RendererDebugProgramOverrides{ + .normals = debug_normals, + .albedo = debug_albedo, + .depth = debug_depth, + .overdraw = debug_overdraw, + }); render::scene::Scene scene; const auto root = scene.create_node("root"); @@ -163,6 +205,7 @@ int main(int argc, char** argv) { while (!runtime.should_quit()) { runtime.begin_frame(); runtime.pump_events(); + apply_debug_mode_hotkeys(runtime.input(), renderer); const auto& window = runtime.window_state(); if (window.resized_this_frame) renderer.request_resize(window.width, window.height); @@ -263,6 +306,7 @@ 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, @@ -273,8 +317,15 @@ int main(int argc, char** argv) { 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) { @@ -290,12 +341,32 @@ int main(int argc, char** argv) { renderer.submit(kMainView, batch.unique_draws.front()); } } + 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 = "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, + }); renderer.end_frame(); runtime.end_frame(); } renderer.destroy_program(program); + renderer.destroy_program(debug_normals); + renderer.destroy_program(debug_albedo); + renderer.destroy_program(debug_depth); + renderer.destroy_program(debug_overdraw); renderer.destroy_buffer(mesh_buffer); renderer.destroy_buffer(index_buffer); renderer.shutdown(); diff --git a/shaders/README.md b/shaders/README.md index 126bf3a..c2bd9e7 100644 --- a/shaders/README.md +++ b/shaders/README.md @@ -49,6 +49,13 @@ Current variant policy: - targeted debug variants (example: `debug_tint`) - no free-form runtime define explosion; each variant must be declared in the manifest +Statement 14 adds canonical renderer debug programs: + +- `renderer_debug_normals` +- `renderer_debug_albedo` +- `renderer_debug_depth` +- `renderer_debug_overdraw` + ## Build integration Use `render_shaders` to compile all declared programs/variants for selected backends. diff --git a/shaders/debug/renderer_debug/fs_albedo.sc b/shaders/debug/renderer_debug/fs_albedo.sc new file mode 100644 index 0000000..143f081 --- /dev/null +++ b/shaders/debug/renderer_debug/fs_albedo.sc @@ -0,0 +1,8 @@ +$input v_color0, v_world_pos + +#include "../../includes/core/common.sh" + +void main() +{ + gl_FragColor = vec4(v_color0.rgb, 1.0); +} diff --git a/shaders/debug/renderer_debug/fs_depth.sc b/shaders/debug/renderer_debug/fs_depth.sc new file mode 100644 index 0000000..3fd9611 --- /dev/null +++ b/shaders/debug/renderer_debug/fs_depth.sc @@ -0,0 +1,9 @@ +$input v_color0, v_world_pos + +#include "../../includes/core/common.sh" + +void main() +{ + float linearDepth = clamp((gl_FragCoord.z - 0.01) / (1.0 - 0.01), 0.0, 1.0); + gl_FragColor = vec4(vec3(1.0 - linearDepth), 1.0); +} diff --git a/shaders/debug/renderer_debug/fs_normals.sc b/shaders/debug/renderer_debug/fs_normals.sc new file mode 100644 index 0000000..ab0345d --- /dev/null +++ b/shaders/debug/renderer_debug/fs_normals.sc @@ -0,0 +1,9 @@ +$input v_color0, v_world_pos + +#include "../../includes/core/common.sh" + +void main() +{ + vec3 n = normalize(v_world_pos * 0.5 + vec3(0.001, 0.001, 1.0)); + gl_FragColor = vec4(n * 0.5 + 0.5, 1.0); +} diff --git a/shaders/debug/renderer_debug/fs_overdraw.sc b/shaders/debug/renderer_debug/fs_overdraw.sc new file mode 100644 index 0000000..e83ccb1 --- /dev/null +++ b/shaders/debug/renderer_debug/fs_overdraw.sc @@ -0,0 +1,8 @@ +$input v_color0, v_world_pos + +#include "../../includes/core/common.sh" + +void main() +{ + gl_FragColor = vec4(0.08, 0.02, 0.01, 0.06); +} diff --git a/shaders/debug/renderer_debug/vs.sc b/shaders/debug/renderer_debug/vs.sc new file mode 100644 index 0000000..9f2705c --- /dev/null +++ b/shaders/debug/renderer_debug/vs.sc @@ -0,0 +1,12 @@ +$input a_position, a_color0 +$output v_color0, v_world_pos + +#include "../../includes/core/common.sh" +#include "../../includes/core/transform.sh" + +void main() +{ + gl_Position = render_transform_position(a_position); + v_world_pos = a_position; + v_color0 = a_color0; +} diff --git a/shaders/includes/varying_color.def.sc b/shaders/includes/varying_color.def.sc index e334ee3..c2e1212 100644 --- a/shaders/includes/varying_color.def.sc +++ b/shaders/includes/varying_color.def.sc @@ -2,3 +2,4 @@ vec3 a_position : POSITION; vec4 a_color0 : COLOR0; vec4 v_color0 : COLOR0; +vec3 v_world_pos : TEXCOORD1; diff --git a/shaders/shaders.cmake b/shaders/shaders.cmake index 9aee5c1..8eecfcf 100644 --- a/shaders/shaders.cmake +++ b/shaders/shaders.cmake @@ -3,6 +3,10 @@ set(RENDER_SHADER_PROGRAMS "debug_triangle|debug|debug/triangle/vs.sc|debug/triangle/fs.sc|default|" "debug_triangle|debug|debug/triangle/vs.sc|debug/triangle/fs.sc|debug_tint|RENDER_DEBUG_TINT=1" + "renderer_debug_normals|debug|debug/renderer_debug/vs.sc|debug/renderer_debug/fs_normals.sc|default|" + "renderer_debug_albedo|debug|debug/renderer_debug/vs.sc|debug/renderer_debug/fs_albedo.sc|default|" + "renderer_debug_depth|debug|debug/renderer_debug/vs.sc|debug/renderer_debug/fs_depth.sc|default|" + "renderer_debug_overdraw|debug|debug/renderer_debug/vs.sc|debug/renderer_debug/fs_overdraw.sc|default|" "material_unlit_color|materials|materials/unlit_color/vs.sc|materials/unlit_color/fs.sc|default|" ) diff --git a/tests/render/debug_renderer_tests.cpp b/tests/render/debug_renderer_tests.cpp new file mode 100644 index 0000000..6f32965 --- /dev/null +++ b/tests/render/debug_renderer_tests.cpp @@ -0,0 +1,44 @@ +#include "engine/render/debug_renderer.hpp" + +#include + +int main() { + using namespace render::rendering; + + RendererDebugState state; + assert(state.mode() == RendererDebugMode::Disabled); + assert(state.set_mode(RendererDebugMode::Normals)); + assert(state.mode() == RendererDebugMode::Normals); + assert(!state.set_mode_from_index(44)); + assert(state.set_mode_from_index(0)); + assert(state.mode() == RendererDebugMode::Disabled); + assert(state.cycle_next_mode()); + assert(state.mode() == RendererDebugMode::Wireframe); + + RendererDebugSnapshot baseline{}; + baseline.backend = RendererBackend::Vulkan; + baseline.backbuffer_width = 1920; + baseline.backbuffer_height = 1080; + baseline.frame_time_ms = 16; + state.set_gpu_timing_supported(false); + state.begin_frame(baseline); + state.set_counters(RendererDebugCounters{ + .visible_directional_lights = 1, + .visible_point_lights = 8, + .submitted_draws = 120, + .instanced_draws = 6, + .submitted_instances = 300, + }); + state.add_pass_timing(RendererPassTiming{.pass_name = "main-lit-pass", .cpu_ms = 1.2F, .gpu_ms = std::nullopt}); + const RendererDebugSnapshot snapshot = state.snapshot(); + assert(snapshot.backbuffer_width == 1920); + assert(snapshot.counters.submitted_draws == 120); + assert(snapshot.timings.size() == 1); + assert(snapshot.timings[0].pass_name == "main-lit-pass"); + assert(!snapshot.timings[0].gpu_ms.has_value()); + + const auto lines = state.build_overlay_lines(); + assert(lines.size() >= 3); + + return 0; +} diff --git a/tests/render/renderer_lifecycle_tests.cpp b/tests/render/renderer_lifecycle_tests.cpp index 948c8f6..f04e60a 100644 --- a/tests/render/renderer_lifecycle_tests.cpp +++ b/tests/render/renderer_lifecycle_tests.cpp @@ -31,6 +31,10 @@ int main() { assert(renderer.state() == RendererLifecycleState::Uninitialized); assert(!renderer.is_ready()); assert(!renderer.can_render()); + assert(renderer.debug_mode() == RendererDebugMode::Disabled); + assert(renderer.set_debug_mode(RendererDebugMode::Depth)); + assert(renderer.debug_mode() == RendererDebugMode::Depth); + assert(!renderer.set_debug_mode_from_index(255)); assert(!renderer.begin_frame()); assert(!renderer.end_frame());