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
12 changes: 11 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ add_library(render_renderer STATIC
engine/render/renderer.hpp
engine/render/renderer_types.hpp
engine/render/buffer_types.hpp
engine/render/buffer_types.cpp
engine/render/draw_submission.hpp
engine/render/draw_submission.cpp
engine/render/shader_types.hpp
engine/render/shader_library.hpp
engine/render/shader_library.cpp
Expand Down Expand Up @@ -204,6 +207,11 @@ if(RENDER_BUILD_TESTS)
render_apply_project_options(render_shader_pipeline_tests)
render_apply_warnings(render_shader_pipeline_tests)

add_executable(render_geometry_submission_tests tests/render/geometry_submission_tests.cpp)
target_link_libraries(render_geometry_submission_tests PRIVATE render::renderer)
render_apply_project_options(render_geometry_submission_tests)
render_apply_warnings(render_geometry_submission_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 @@ -221,14 +229,16 @@ if(RENDER_BUILD_TESTS)
set_tests_properties(unit.renderer.lifecycle PROPERTIES LABELS "unit;renderer")
add_test(NAME unit.renderer.shader_pipeline COMMAND render_shader_pipeline_tests)
set_tests_properties(unit.renderer.shader_pipeline PROPERTIES LABELS "unit;renderer;shader")
add_test(NAME unit.renderer.geometry_submission COMMAND render_geometry_submission_tests)
set_tests_properties(unit.renderer.geometry_submission PROPERTIES LABELS "unit;renderer;geometry")
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_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_scene_tests
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
COMMENT "Running render unit tests"
)
Expand Down
62 changes: 62 additions & 0 deletions docs/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,65 @@ Deferred to later statements:
- draw submissions are emitted from `Scene::collect_visible_renderables(...)`

This establishes a stable engine-owned scene/runtime boundary above the renderer API.

## Geometry submission foundation (Statement 12)

Statement 12 introduces an engine-owned geometry submission path designed for procedural workloads.

### Implemented renderer-side concepts

- `VertexLayoutDescription` with explicit offsets/stride and validation.
- `MeshBufferDescription` / `IndexBufferDescription` with usage intent and debug metadata.
- `InstanceBufferDescription` abstraction for reusable instance payload uploads.
- CPU mesh bridge through `CpuMeshData` + `validate_cpu_mesh_data(...)`.
- `MaterialBinding` wrapping program + parameter hash (with texture slots reserved).
- `DrawSubmission` and `DrawBatch` structures for explicit per-draw/per-batch inspection.

### Batching rules

Current batching key is the tuple:

- view id
- mesh buffer handle
- index buffer handle
- program handle
- render state flags
- material parameter hash

Draws sharing the same key are grouped together. If group size is >= `BatchPolicy::min_instanced_count` (default 4), the renderer emits instanced draws; otherwise each draw is submitted uniquely.

This creates deterministic, stable grouping and keeps a clear path for future auto-batching improvements.

### Instancing policy

- Preferred path for repeated procedural objects is instancing.
- One-off or small groups remain unique draws.
- `BatchPolicy::max_instances_per_draw` caps batch size and splits very large groups into multiple instanced draw calls.

### Scene integration

Scene renderables now carry renderer-owned resource bindings:

- `mesh_buffer` + optional `index_buffer/index_count`
- `material` (`MaterialBinding`)
- `draw_state`

`render_shell` extracts scene-visible renderables, creates `DrawSubmission` records, runs batch planning, and submits unique or instanced draws via renderer API.

### Diagnostics

`SubmissionDiagnostics` reports:

- submitted draw count
- instanced draw count
- unique draw count
- submitted instance total
- batch count

These diagnostics are intended for profiling/debug overlays and frame-level instrumentation.

### Deferred follow-ups

- Dynamic GPU buffer update path currently uses a placeholder API contract and is staged for a follow-up statement.
- Full material parameter/uniform binding and texture set binding beyond fixed slots is deferred.
- GPU-driven culling/indirect draw remains deferred by design.
11 changes: 11 additions & 0 deletions engine/render/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,14 @@ The public renderer API now exposes an explicit lifecycle and frame contract:
- Recovery hook: `try_recover`

This keeps app/runtime code independent of bgfx lifecycle details.

## Geometry submission layer (Statement 12)

New engine-owned geometry submission files:

- `buffer_types.*`: vertex/index/instance descriptors, CPU mesh validation, material binding.
- `draw_submission.*`: draw submission records, batching keys, instancing policy, submission diagnostics.

`Renderer` now exposes mesh/index/instance buffer creation + update hooks and both unique and instanced submission entry points.

bgfx handles remain internal to backend implementation (`engine/render/bgfx/renderer_bgfx.cpp`).
115 changes: 104 additions & 11 deletions engine/render/bgfx/renderer_bgfx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@

#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <unordered_map>
#include <vector>

namespace render::rendering {
namespace {
Expand Down Expand Up @@ -69,8 +72,16 @@ bgfx::Attrib::Enum to_bgfx_attrib(const VertexAttribute attribute) {
case VertexAttribute::Position: return bgfx::Attrib::Position;
case VertexAttribute::Normal: return bgfx::Attrib::Normal;
case VertexAttribute::Tangent: return bgfx::Attrib::Tangent;
case VertexAttribute::Bitangent: return bgfx::Attrib::Bitangent;
case VertexAttribute::Color0: return bgfx::Attrib::Color0;
case VertexAttribute::TexCoord0: return bgfx::Attrib::TexCoord0;
case VertexAttribute::TexCoord1: return bgfx::Attrib::TexCoord1;
case VertexAttribute::InstanceRow0: return bgfx::Attrib::TexCoord4;
case VertexAttribute::InstanceRow1: return bgfx::Attrib::TexCoord5;
case VertexAttribute::InstanceRow2: return bgfx::Attrib::TexCoord6;
case VertexAttribute::InstanceRow3: return bgfx::Attrib::TexCoord7;
case VertexAttribute::InstanceColor: return bgfx::Attrib::Color1;
case VertexAttribute::InstanceData0: return bgfx::Attrib::TexCoord3;
default: return bgfx::Attrib::Position;
}
}
Expand Down Expand Up @@ -178,6 +189,13 @@ struct Renderer::Impl {
std::uint64_t frame_count{0};
std::chrono::steady_clock::time_point frame_begin_time{};
std::uint32_t last_frame_time_ms{0};
std::uint16_t next_instance_buffer{0};
struct InstanceBufferStorage {
std::uint16_t stride{64};
std::uint32_t capacity{0};
std::vector<std::byte> bytes{};
};
std::unordered_map<std::uint16_t, InstanceBufferStorage> instance_buffers{};

[[nodiscard]] bool is_initialized() const noexcept {
return bgfx_initialized;
Expand Down Expand Up @@ -551,8 +569,13 @@ void Renderer::set_view_transform(
bgfx::setViewTransform(view.value, view_transform.data(), projection.data());
}

VertexBufferHandle Renderer::create_vertex_buffer(const VertexBufferDescription& desc) {
if (!impl_->can_render() || desc.data.empty()) {
MeshBufferHandle Renderer::create_mesh_buffer(const MeshBufferDescription& desc) {
if (!impl_->can_render() || desc.data.empty() || desc.vertex_count == 0) {
return {};
}
const VertexLayoutValidation layout_validation = validate_vertex_layout(desc.layout);
if (!layout_validation.valid) {
platform::log::error(std::string{"Mesh buffer creation failed: "} + layout_validation.reason);
return {};
}

Expand All @@ -570,19 +593,59 @@ VertexBufferHandle Renderer::create_vertex_buffer(const VertexBufferDescription&

const bgfx::Memory* memory = bgfx::copy(desc.data.data(), static_cast<uint32_t>(desc.data.size_bytes()));
const bgfx::VertexBufferHandle handle = bgfx::createVertexBuffer(memory, layout);
return VertexBufferHandle{handle.idx};
return MeshBufferHandle{handle.idx};
}

bool Renderer::update_mesh_buffer(const MeshBufferHandle handle, const MeshBufferUpdate& update) {
static_cast<void>(handle);
static_cast<void>(update);
return true;
}

IndexBufferHandle Renderer::create_index_buffer(const IndexBufferDescription& desc) {
if (!impl_->can_render() || desc.data.empty()) {
if (!impl_->can_render() || desc.data.empty() || desc.index_count == 0) {
return {};
}

const bgfx::Memory* memory = bgfx::copy(desc.data.data(), static_cast<uint32_t>(desc.data.size_bytes()));
const bgfx::IndexBufferHandle handle = bgfx::createIndexBuffer(memory);
const std::uint16_t flags = desc.index_type == IndexType::Uint32 ? BGFX_BUFFER_INDEX32 : BGFX_BUFFER_NONE;
const bgfx::IndexBufferHandle handle = bgfx::createIndexBuffer(memory, flags);
return IndexBufferHandle{handle.idx};
}

bool Renderer::update_index_buffer(const IndexBufferHandle handle, const IndexBufferUpdate& update) {
static_cast<void>(handle);
static_cast<void>(update);
return true;
}

InstanceBufferHandle Renderer::create_instance_buffer(const InstanceBufferDescription& desc) {
if (desc.stride == 0 || desc.capacity == 0) {
return {};
}
const std::uint16_t handle = ++impl_->next_instance_buffer;
auto& storage = impl_->instance_buffers[handle];
storage.stride = desc.stride;
storage.capacity = desc.capacity;
if (!desc.initial_data.empty() && desc.initial_count > 0) {
storage.bytes.assign(desc.initial_data.begin(), desc.initial_data.end());
}
return InstanceBufferHandle{handle};
}

bool Renderer::update_instance_buffer(const InstanceBufferHandle handle, const InstanceBufferUpdate& update) {
auto it = impl_->instance_buffers.find(handle.idx);
if (it == impl_->instance_buffers.end() || update.stride == 0 || update.count == 0 || update.data.empty()) {
return false;
}
auto& storage = it->second;
if (update.count > storage.capacity || update.stride != storage.stride) {
return false;
}
storage.bytes.assign(update.data.begin(), update.data.end());
return true;
}

ProgramHandle Renderer::create_program(const ShaderProgramDescription& desc) {
if (!impl_->can_render()) {
return {};
Expand Down Expand Up @@ -652,7 +715,7 @@ TextureHandle Renderer::create_solid_color_texture(const SolidColorTextureDescri
return TextureHandle{handle.idx};
}

void Renderer::destroy_buffer(const VertexBufferHandle handle) {
void Renderer::destroy_buffer(const MeshBufferHandle handle) {
if (impl_->lifecycle == RendererLifecycleState::Ready && handle.idx != kInvalidHandle) {
bgfx::destroy(bgfx::VertexBufferHandle{handle.idx});
}
Expand All @@ -664,6 +727,10 @@ void Renderer::destroy_buffer(const IndexBufferHandle handle) {
}
}

void Renderer::destroy_buffer(const InstanceBufferHandle handle) {
impl_->instance_buffers.erase(handle.idx);
}

void Renderer::destroy_program(const ProgramHandle handle) {
if (impl_->lifecycle == RendererLifecycleState::Ready && handle.idx != kInvalidHandle) {
bgfx::destroy(bgfx::ProgramHandle{handle.idx});
Expand All @@ -676,25 +743,51 @@ void Renderer::destroy_texture(const TextureHandle handle) {
}
}

void Renderer::submit(const ViewId view, const MeshSubmission& mesh_submission) {
void Renderer::submit(const ViewId view, const DrawSubmission& mesh_submission) {
if (!impl_->can_render()) {
return;
}

if (mesh_submission.mesh.vertex_buffer.idx == kInvalidHandle || mesh_submission.mesh.index_buffer.idx == kInvalidHandle
|| mesh_submission.program.idx == kInvalidHandle) {
if (mesh_submission.mesh.vertex_buffer.idx == kInvalidHandle || mesh_submission.material.program.idx == kInvalidHandle) {
return;
}

bgfx::setTransform(mesh_submission.transform.data());
bgfx::setVertexBuffer(0, bgfx::VertexBufferHandle{mesh_submission.mesh.vertex_buffer.idx});
bgfx::setIndexBuffer(bgfx::IndexBufferHandle{mesh_submission.mesh.index_buffer.idx}, 0, mesh_submission.mesh.index_count);
if (mesh_submission.mesh.index_buffer.idx != kInvalidHandle && mesh_submission.mesh.index_count > 0) {
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
? 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);
bgfx::setState(state);
bgfx::submit(view.value, bgfx::ProgramHandle{mesh_submission.program.idx});
bgfx::submit(view.value, bgfx::ProgramHandle{mesh_submission.material.program.idx});
}

void Renderer::submit_instanced(const ViewId view, const DrawSubmission& submission, const std::span<const float> transforms) {
if (!impl_->can_render() || transforms.empty() || submission.mesh.vertex_buffer.idx == kInvalidHandle
|| submission.material.program.idx == kInvalidHandle || (transforms.size() % 16U) != 0U) {
return;
}

const std::uint32_t count = static_cast<std::uint32_t>(transforms.size() / 16U);
bgfx::InstanceDataBuffer idb{};
if (!bgfx::allocInstanceDataBuffer(&idb, count, 64)) {
return;
}
std::memcpy(idb.data, transforms.data(), static_cast<std::size_t>(count) * 64U);

bgfx::setVertexBuffer(0, bgfx::VertexBufferHandle{submission.mesh.vertex_buffer.idx});
if (submission.mesh.index_buffer.idx != kInvalidHandle && submission.mesh.index_count > 0) {
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
? 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);
bgfx::setState(state);
bgfx::submit(view.value, bgfx::ProgramHandle{submission.material.program.idx});
}

} // namespace render::rendering
Loading
Loading