Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -240,14 +247,16 @@ 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)
set_tests_properties(headless.smoke.startup PROPERTIES LABELS "headless;smoke")

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"
)
Expand Down
50 changes: 50 additions & 0 deletions docs/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
27 changes: 27 additions & 0 deletions engine/render/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
118 changes: 112 additions & 6 deletions engine/render/bgfx/renderer_bgfx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <cstring>
#include <filesystem>
#include <fstream>
#include <optional>
#include <sstream>
#include <unordered_map>
#include <vector>
Expand Down Expand Up @@ -196,6 +197,7 @@ struct Renderer::Impl {
std::vector<std::byte> bytes{};
};
std::unordered_map<std::uint16_t, InstanceBufferStorage> instance_buffers{};
RendererDebugState debug_state{};

[[nodiscard]] bool is_initialized() const noexcept {
return bgfx_initialized;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<std::uint16_t>(i), 0x0f, "%s", overlay_lines[i].c_str());
}
} else {
bgfx::setDebug(BGFX_DEBUG_NONE);
}

return true;
Expand Down Expand Up @@ -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<RendererDebugMode> mode = renderer_debug_mode_from_index(index);
if (!mode.has_value()) {
std::ostringstream stream;
stream << "Renderer debug mode index out of range: " << static_cast<std::uint32_t>(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) {
Expand Down Expand Up @@ -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<std::uint64_t>(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<const float> transforms) {
Expand All @@ -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<std::uint64_t>(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
Loading
Loading