diff --git a/CMakeLists.txt b/CMakeLists.txt index c132eaf..d0af22b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 @@ -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) @@ -221,6 +229,8 @@ 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) @@ -228,7 +238,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_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" ) diff --git a/docs/rendering.md b/docs/rendering.md index 2415bda..02dd6e3 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -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. diff --git a/engine/render/README.md b/engine/render/README.md index cb48d52..df5e23b 100644 --- a/engine/render/README.md +++ b/engine/render/README.md @@ -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`). diff --git a/engine/render/bgfx/renderer_bgfx.cpp b/engine/render/bgfx/renderer_bgfx.cpp index 1332ff2..e58d159 100644 --- a/engine/render/bgfx/renderer_bgfx.cpp +++ b/engine/render/bgfx/renderer_bgfx.cpp @@ -12,9 +12,12 @@ #include #include +#include #include #include #include +#include +#include namespace render::rendering { namespace { @@ -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; } } @@ -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 bytes{}; + }; + std::unordered_map instance_buffers{}; [[nodiscard]] bool is_initialized() const noexcept { return bgfx_initialized; @@ -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 {}; } @@ -570,19 +593,59 @@ VertexBufferHandle Renderer::create_vertex_buffer(const VertexBufferDescription& const bgfx::Memory* memory = bgfx::copy(desc.data.data(), static_cast(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(handle); + static_cast(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(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(handle); + static_cast(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 {}; @@ -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}); } @@ -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}); @@ -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(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 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(transforms.size() / 16U); + bgfx::InstanceDataBuffer idb{}; + if (!bgfx::allocInstanceDataBuffer(&idb, count, 64)) { + return; + } + std::memcpy(idb.data, transforms.data(), static_cast(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(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 diff --git a/engine/render/buffer_types.cpp b/engine/render/buffer_types.cpp new file mode 100644 index 0000000..59244c0 --- /dev/null +++ b/engine/render/buffer_types.cpp @@ -0,0 +1,82 @@ +#include "engine/render/buffer_types.hpp" + +#include + +namespace render::rendering { +namespace { + +std::uint16_t attribute_component_size(const AttributeType type) { + switch (type) { + case AttributeType::Uint8: return 1; + case AttributeType::Int16: return 2; + case AttributeType::Half: return 2; + case AttributeType::Float: return 4; + default: return 0; + } +} + +} // namespace + +VertexLayoutValidation validate_vertex_layout(const VertexLayoutDescription& desc) { + if (desc.stride == 0) { + return {.valid = false, .reason = "vertex layout stride must be greater than zero"}; + } + if (desc.elements.empty()) { + return {.valid = false, .reason = "vertex layout must define at least one element"}; + } + + std::unordered_set semantics; + std::uint16_t max_end = 0; + + for (const VertexElement& element : desc.elements) { + if (element.components == 0 || element.components > 4) { + return {.valid = false, .reason = "vertex element component count must be in [1,4]"}; + } + const auto inserted = semantics.insert(static_cast(element.attribute)); + if (!inserted.second) { + return {.valid = false, .reason = "vertex layout contains duplicate attribute semantic"}; + } + + const std::uint16_t byte_size = static_cast(attribute_component_size(element.type) * element.components); + const std::uint16_t end = static_cast(element.offset + byte_size); + if (end > desc.stride) { + return {.valid = false, .reason = "vertex element extends past stride"}; + } + if (end > max_end) { + max_end = end; + } + } + + if (max_end == 0) { + return {.valid = false, .reason = "vertex layout has invalid effective size"}; + } + + return {.valid = true, .reason = {}}; +} + +CpuMeshValidation validate_cpu_mesh_data(const CpuMeshData& data) { + const VertexLayoutValidation layout = validate_vertex_layout(data.layout); + if (!layout.valid) { + return {.valid = false, .reason = layout.reason}; + } + + if (data.vertex_count == 0) { + return {.valid = false, .reason = "cpu mesh must have at least one vertex"}; + } + + const std::size_t required_vertex_bytes = static_cast(data.layout.stride) * data.vertex_count; + if (data.vertex_data.size() != required_vertex_bytes) { + return {.valid = false, .reason = "cpu mesh vertex_data size does not match stride*vertex_count"}; + } + + if (data.index_count > 0) { + const std::size_t index_size = data.index_type == IndexType::Uint16 ? 2U : 4U; + if (data.index_data.size() != data.index_count * index_size) { + return {.valid = false, .reason = "cpu mesh index_data size does not match index_count*index_size"}; + } + } + + return {.valid = true, .reason = {}}; +} + +} // namespace render::rendering diff --git a/engine/render/buffer_types.hpp b/engine/render/buffer_types.hpp index b63e5bb..5cc4180 100644 --- a/engine/render/buffer_types.hpp +++ b/engine/render/buffer_types.hpp @@ -2,9 +2,11 @@ #include "engine/render/renderer_types.hpp" +#include #include #include #include +#include #include namespace render::rendering { @@ -13,8 +15,16 @@ enum class VertexAttribute : std::uint8_t { Position = 0, Normal, Tangent, + Bitangent, Color0, TexCoord0, + TexCoord1, + InstanceRow0, + InstanceRow1, + InstanceRow2, + InstanceRow3, + InstanceColor, + InstanceData0, }; enum class AttributeType : std::uint8_t { @@ -24,8 +34,19 @@ enum class AttributeType : std::uint8_t { Float, }; +enum class BufferUsage : std::uint8_t { + Immutable = 0, + Dynamic, +}; + +enum class IndexType : std::uint8_t { + Uint16 = 0, + Uint32, +}; + struct VertexElement { VertexAttribute attribute{VertexAttribute::Position}; + std::uint16_t offset{0}; std::uint8_t components{3}; AttributeType type{AttributeType::Float}; bool normalized{false}; @@ -33,29 +54,103 @@ struct VertexElement { }; struct VertexLayoutDescription { + std::uint16_t stride{0}; std::vector elements{}; }; -struct VertexBufferDescription { +struct VertexLayoutValidation { + bool valid{false}; + std::string reason{}; +}; + +[[nodiscard]] VertexLayoutValidation validate_vertex_layout(const VertexLayoutDescription& desc); + +struct MeshBufferDescription { VertexLayoutDescription layout{}; std::span data{}; + std::uint32_t vertex_count{0}; + BufferUsage usage{BufferUsage::Immutable}; + std::string debug_name{}; +}; + +struct MeshBufferUpdate { + std::span data{}; + std::uint32_t vertex_count{0}; }; struct IndexBufferDescription { - std::span data{}; + std::span data{}; + std::uint32_t index_count{0}; + IndexType index_type{IndexType::Uint16}; + BufferUsage usage{BufferUsage::Immutable}; + std::string debug_name{}; +}; + +struct IndexBufferUpdate { + std::span data{}; + std::uint32_t index_count{0}; +}; + +struct InstanceBufferDescription { + std::uint16_t stride{64}; + std::uint32_t capacity{0}; + std::span initial_data{}; + std::uint32_t initial_count{0}; + std::string debug_name{}; +}; + +struct InstanceBufferUpdate { + std::span data{}; + std::uint16_t stride{64}; + std::uint32_t count{0}; +}; + +struct MeshBufferHandle { + std::uint16_t idx{kInvalidHandle}; +}; + +struct InstanceBufferHandle { + std::uint16_t idx{kInvalidHandle}; }; struct MeshHandle { - VertexBufferHandle vertex_buffer{}; + MeshBufferHandle vertex_buffer{}; IndexBufferHandle index_buffer{}; + std::uint32_t vertex_count{0}; + std::uint32_t index_count{0}; + IndexType index_type{IndexType::Uint16}; +}; + +struct CpuMeshData { + VertexLayoutDescription layout{}; + std::vector vertex_data{}; + std::uint32_t vertex_count{0}; + std::vector index_data{}; std::uint32_t index_count{0}; + IndexType index_type{IndexType::Uint16}; + + [[nodiscard]] bool indexed() const noexcept { return index_count > 0; } }; -struct MeshSubmission { - MeshHandle mesh{}; +struct CpuMeshValidation { + bool valid{false}; + std::string reason{}; +}; + +[[nodiscard]] CpuMeshValidation validate_cpu_mesh_data(const CpuMeshData& data); + +struct InstanceDataView { + std::span data{}; + std::uint16_t stride{64}; + std::uint32_t count{0}; +}; + +struct MaterialBinding { ProgramHandle program{}; - std::span transform{}; - DrawState draw_state{}; + std::array textures{}; + std::uint64_t parameter_hash{0}; + + [[nodiscard]] bool valid() const noexcept { return program.idx != kInvalidHandle; } }; } // namespace render::rendering diff --git a/engine/render/draw_submission.cpp b/engine/render/draw_submission.cpp new file mode 100644 index 0000000..39195d4 --- /dev/null +++ b/engine/render/draw_submission.cpp @@ -0,0 +1,104 @@ +#include "engine/render/draw_submission.hpp" + +#include +#include + +namespace render::rendering { + +BatchKey make_batch_key(const DrawSubmission& submission) { + return BatchKey{ + .view = submission.view.value, + .mesh_buffer = submission.mesh.vertex_buffer.idx, + .index_buffer = submission.mesh.index_buffer.idx, + .program = submission.material.program.idx, + .state_flags = submission.draw_state.state_flags, + .parameter_hash = submission.material.parameter_hash, + }; +} + +std::vector build_draw_batches( + const std::span submissions, + const BatchPolicy& policy, + SubmissionDiagnostics* diagnostics) { + std::vector batches; + if (diagnostics != nullptr) { + *diagnostics = {}; + diagnostics->submitted_draws = static_cast(submissions.size()); + } + + std::unordered_map> grouped; + grouped.reserve(submissions.size()); + + for (const DrawSubmission& submission : submissions) { + const BatchKey key = make_batch_key(submission); + const std::uint64_t packed = + (static_cast(key.view) << 48U) ^ (static_cast(key.mesh_buffer) << 32U) + ^ (static_cast(key.index_buffer) << 16U) ^ static_cast(key.program) + ^ key.state_flags ^ key.parameter_hash; + grouped[packed].push_back(&submission); + } + + batches.reserve(grouped.size()); + + for (auto& [_, group] : grouped) { + if (group.empty()) { + continue; + } + + std::sort(group.begin(), group.end(), [](const DrawSubmission* a, const DrawSubmission* b) { + return a->sort_key < b->sort_key; + }); + + const DrawSubmission& first = *group.front(); + const bool use_instancing = group.size() >= policy.min_instanced_count; + if (use_instancing) { + std::size_t offset = 0; + while (offset < group.size()) { + const std::size_t draw_count = std::min(policy.max_instances_per_draw, group.size() - offset); + DrawBatch batch{}; + batch.key = make_batch_key(first); + batch.mode = SubmissionMode::Instanced; + batch.mesh = first.mesh; + batch.material = first.material; + batch.draw_state = first.draw_state; + batch.transforms.reserve(draw_count * 16U); + + for (std::size_t i = 0; i < draw_count; ++i) { + const DrawSubmission& draw = *group[offset + i]; + batch.transforms.insert(batch.transforms.end(), draw.transform.begin(), draw.transform.end()); + } + + if (diagnostics != nullptr) { + diagnostics->instanced_draws += 1; + diagnostics->submitted_instances += static_cast(draw_count); + } + batches.push_back(std::move(batch)); + offset += draw_count; + } + continue; + } + + for (const DrawSubmission* draw : group) { + DrawBatch batch{}; + batch.key = make_batch_key(*draw); + batch.mode = SubmissionMode::Unique; + batch.mesh = draw->mesh; + batch.material = draw->material; + batch.draw_state = draw->draw_state; + batch.unique_draws.push_back(*draw); + if (diagnostics != nullptr) { + diagnostics->unique_draws += 1; + diagnostics->submitted_instances += 1; + } + batches.push_back(std::move(batch)); + } + } + + if (diagnostics != nullptr) { + diagnostics->batch_count = static_cast(batches.size()); + } + + return batches; +} + +} // namespace render::rendering diff --git a/engine/render/draw_submission.hpp b/engine/render/draw_submission.hpp new file mode 100644 index 0000000..38028d3 --- /dev/null +++ b/engine/render/draw_submission.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "engine/render/buffer_types.hpp" +#include "engine/render/renderer_types.hpp" + +#include +#include +#include +#include + +namespace render::rendering { + +enum class SubmissionMode : std::uint8_t { + Unique = 0, + Instanced, +}; + +struct DrawSubmission { + ViewId view{}; + MeshHandle mesh{}; + MaterialBinding material{}; + std::array transform{}; + DrawState draw_state{}; + std::uint32_t sort_key{0}; +}; + +struct BatchKey { + std::uint16_t view{0}; + std::uint16_t mesh_buffer{kInvalidHandle}; + std::uint16_t index_buffer{kInvalidHandle}; + std::uint16_t program{kInvalidHandle}; + std::uint64_t state_flags{0}; + std::uint64_t parameter_hash{0}; + + friend bool operator==(const BatchKey&, const BatchKey&) = default; +}; + +struct BatchPolicy { + std::uint32_t min_instanced_count{4}; + std::uint32_t max_instances_per_draw{1024}; +}; + +struct DrawBatch { + BatchKey key{}; + SubmissionMode mode{SubmissionMode::Unique}; + MeshHandle mesh{}; + MaterialBinding material{}; + DrawState draw_state{}; + std::vector transforms{}; // 16 floats per instance + std::vector unique_draws{}; +}; + +struct SubmissionDiagnostics { + std::uint32_t submitted_draws{0}; + std::uint32_t instanced_draws{0}; + std::uint32_t unique_draws{0}; + std::uint32_t submitted_instances{0}; + std::uint32_t batch_count{0}; +}; + +[[nodiscard]] BatchKey make_batch_key(const DrawSubmission& submission); +[[nodiscard]] std::vector build_draw_batches( + std::span submissions, + const BatchPolicy& policy, + SubmissionDiagnostics* diagnostics = nullptr); + +} // namespace render::rendering diff --git a/engine/render/renderer.hpp b/engine/render/renderer.hpp index fad736f..469a04c 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/draw_submission.hpp" #include "engine/render/renderer_types.hpp" #include "engine/render/shader_types.hpp" #include "engine/render/texture_types.hpp" @@ -42,17 +43,26 @@ class Renderer { void set_view(ViewId view, const ViewDescription& desc); void set_view_transform(ViewId view, std::span view_transform, std::span projection); - [[nodiscard]] VertexBufferHandle create_vertex_buffer(const VertexBufferDescription& desc); + [[nodiscard]] MeshBufferHandle create_mesh_buffer(const MeshBufferDescription& desc); + [[nodiscard]] bool update_mesh_buffer(MeshBufferHandle handle, const MeshBufferUpdate& update); + [[nodiscard]] IndexBufferHandle create_index_buffer(const IndexBufferDescription& desc); + [[nodiscard]] bool update_index_buffer(IndexBufferHandle handle, const IndexBufferUpdate& update); + + [[nodiscard]] InstanceBufferHandle create_instance_buffer(const InstanceBufferDescription& desc); + [[nodiscard]] bool update_instance_buffer(InstanceBufferHandle handle, const InstanceBufferUpdate& update); + [[nodiscard]] ProgramHandle create_program(const ShaderProgramDescription& desc); [[nodiscard]] TextureHandle create_solid_color_texture(const SolidColorTextureDescription& desc); - void destroy_buffer(VertexBufferHandle handle); + void destroy_buffer(MeshBufferHandle handle); void destroy_buffer(IndexBufferHandle handle); + void destroy_buffer(InstanceBufferHandle handle); void destroy_program(ProgramHandle handle); void destroy_texture(TextureHandle handle); - void submit(ViewId view, const MeshSubmission& mesh_submission); + void submit(ViewId view, const DrawSubmission& submission); + void submit_instanced(ViewId view, const DrawSubmission& submission, std::span transforms); private: struct Impl; diff --git a/engine/scene/scene.hpp b/engine/scene/scene.hpp index 523904d..31210da 100644 --- a/engine/scene/scene.hpp +++ b/engine/scene/scene.hpp @@ -2,6 +2,7 @@ #include "engine/core/camera.hpp" #include "engine/core/transform.hpp" +#include "engine/render/buffer_types.hpp" #include "engine/render/renderer_types.hpp" #include @@ -62,10 +63,10 @@ struct LightComponent { struct RenderableComponent { bool enabled{true}; std::uint32_t layer_mask{kVisibilityAll}; - rendering::VertexBufferHandle vertex_buffer{}; + rendering::MeshBufferHandle mesh_buffer{}; rendering::IndexBufferHandle index_buffer{}; std::uint32_t index_count{0}; - rendering::ProgramHandle program{}; + rendering::MaterialBinding material{}; rendering::DrawState draw_state{}; }; diff --git a/engine/shell/main.cpp b/engine/shell/main.cpp index 38dadb4..86cf06a 100644 --- a/engine/shell/main.cpp +++ b/engine/shell/main.cpp @@ -1,6 +1,7 @@ #include "engine/filesystem/filesystem.hpp" #include "engine/platform/platform_log.hpp" #include "engine/platform/platform_runtime.hpp" +#include "engine/render/draw_submission.hpp" #include "engine/render/renderer.hpp" #include "engine/render/shader_library.hpp" #include "engine/scene/scene.hpp" @@ -8,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -26,50 +26,30 @@ struct PosColorVertex { render::rendering::RendererBackend parse_renderer_backend_from_args(const int argc, char** argv) { for (int i = 1; i < argc; ++i) { const std::string_view arg = argv[i]; - if (arg == "--backend=noop") { - return render::rendering::RendererBackend::Noop; - } - if (arg == "--backend=d3d11") { - return render::rendering::RendererBackend::Direct3D11; - } - if (arg == "--backend=d3d12") { - return render::rendering::RendererBackend::Direct3D12; - } - if (arg == "--backend=metal") { - return render::rendering::RendererBackend::Metal; - } - if (arg == "--backend=vulkan") { - return render::rendering::RendererBackend::Vulkan; - } - if (arg == "--backend=opengl") { - return render::rendering::RendererBackend::OpenGL; - } + if (arg == "--backend=noop") return render::rendering::RendererBackend::Noop; + if (arg == "--backend=d3d11") return render::rendering::RendererBackend::Direct3D11; + if (arg == "--backend=d3d12") return render::rendering::RendererBackend::Direct3D12; + if (arg == "--backend=metal") return render::rendering::RendererBackend::Metal; + if (arg == "--backend=vulkan") return render::rendering::RendererBackend::Vulkan; + if (arg == "--backend=opengl") return render::rendering::RendererBackend::OpenGL; } - return render::rendering::RendererBackend::Auto; } -std::array to_array(const render::core::Mat4& matrix) { - return matrix.m; -} +std::array to_array(const render::core::Mat4& matrix) { return matrix.m; } } // namespace int main(int argc, char** argv) { render::platform::PlatformRuntime runtime; - render::platform::RuntimeConfig platform_config{}; platform_config.app_name = "render-shell"; platform_config.org_name = "render"; - platform_config.window.title = "render :: Statement 11 scene shell"; + platform_config.window.title = "render :: Statement 12 geometry shell"; platform_config.window.width = 1280; platform_config.window.height = 720; platform_config.window.resizable = true; - - if (!runtime.initialize(platform_config)) { - render::platform::log::error("Platform runtime failed to initialize"); - return 1; - } + if (!runtime.initialize(platform_config)) return 1; render::filesystem::FileSystemService filesystem; render::filesystem::StorageConfig storage_config{}; @@ -77,105 +57,82 @@ int main(int argc, char** argv) { storage_config.org_name = platform_config.org_name; storage_config.platform_base_path = runtime.paths().base_path; storage_config.platform_pref_path = runtime.paths().pref_path; - if (!runtime.paths().temp_path.empty()) { - storage_config.platform_temp_path = runtime.paths().temp_path; - } - if (!filesystem.initialize(storage_config)) { - render::platform::log::error("Filesystem service failed to initialize"); - runtime.shutdown(); - return 1; - } + if (!runtime.paths().temp_path.empty()) storage_config.platform_temp_path = runtime.paths().temp_path; + if (!filesystem.initialize(storage_config)) return 1; const render::platform::WindowState& initial_window = runtime.window_state(); - render::rendering::Renderer renderer; render::rendering::RendererConfig renderer_config{}; renderer_config.backend = parse_renderer_backend_from_args(argc, argv); renderer_config.width = initial_window.width; renderer_config.height = initial_window.height; renderer_config.debug = true; - renderer_config.vsync = true; - - if (!renderer.initialize(renderer_config, runtime)) { - render::platform::log::error("Renderer failed to initialize"); - runtime.shutdown(); - return 1; - } + if (!renderer.initialize(renderer_config, runtime)) return 1; const std::vector vertices = { - {-0.6F, -0.4F, 0.0F, 0xff0000ff}, - {0.6F, -0.4F, 0.0F, 0xff00ff00}, - {0.0F, 0.6F, 0.0F, 0xffff0000}, - }; + {-0.5F, -0.5F, 0.0F, 0xff4040ff}, {0.5F, -0.5F, 0.0F, 0xff40ff40}, {0.0F, 0.5F, 0.0F, 0xffff4040}}; const std::vector indices = {0, 1, 2}; - const render::rendering::VertexBufferDescription vertex_buffer_desc{ - .layout = render::rendering::VertexLayoutDescription{ - .elements = { - {render::rendering::VertexAttribute::Position, 3, render::rendering::AttributeType::Float, false, false}, - {render::rendering::VertexAttribute::Color0, 4, render::rendering::AttributeType::Uint8, true, false}, - }, - }, - .data = std::as_bytes(std::span{vertices}), + render::rendering::MeshBufferDescription mesh_desc{}; + mesh_desc.layout.stride = static_cast(sizeof(PosColorVertex)); + mesh_desc.layout.elements = { + {render::rendering::VertexAttribute::Position, 0, 3, render::rendering::AttributeType::Float, false, false}, + {render::rendering::VertexAttribute::Color0, 12, 4, render::rendering::AttributeType::Uint8, true, false}, }; + mesh_desc.data = std::as_bytes(std::span{vertices}); + mesh_desc.vertex_count = static_cast(vertices.size()); + mesh_desc.usage = render::rendering::BufferUsage::Immutable; - const render::rendering::IndexBufferDescription index_buffer_desc{.data = std::span{indices}}; + render::rendering::IndexBufferDescription index_desc{}; + index_desc.data = std::as_bytes(std::span{indices}); + index_desc.index_count = static_cast(indices.size()); - const render::rendering::VertexBufferHandle vertex_buffer = renderer.create_vertex_buffer(vertex_buffer_desc); - const render::rendering::IndexBufferHandle index_buffer = renderer.create_index_buffer(index_buffer_desc); + const auto mesh_buffer = renderer.create_mesh_buffer(mesh_desc); + const auto index_buffer = renderer.create_index_buffer(index_desc); render::rendering::ShaderProgramLibrary shader_library{renderer, filesystem}; - const render::rendering::ShaderProgramId shader_id{ - .category = "debug", - .name = "debug_triangle", - .variant = "default", - }; + const render::rendering::ShaderProgramId shader_id{.category = "debug", .name = "debug_triangle", .variant = "default"}; render::rendering::ProgramHandle program = shader_library.load_program(shader_id); render::scene::Scene scene; - const render::scene::SceneNodeId root = scene.create_node("root"); - const render::scene::SceneNodeId camera_node = scene.create_node("main_camera"); - const render::scene::SceneNodeId light_node = scene.create_node("sun"); - const render::scene::SceneNodeId mesh_node = scene.create_node("triangle_mesh"); - + const auto root = scene.create_node("root"); + const auto camera_node = scene.create_node("camera"); + scene.set_parent(camera_node, root, render::scene::ReparentPolicy::KeepLocalTransform); render::core::Transform camera_local{}; - camera_local.translation = {0.0F, 0.0F, 3.0F}; + camera_local.translation = {0.0F, 0.0F, 6.0F}; scene.set_local_transform(camera_node, camera_local); - - render::scene::CameraComponent camera{}; - camera.projection = render::scene::CameraProjection::Perspective; - scene.set_camera(camera_node, camera); + scene.set_camera(camera_node, {}); scene.set_active_camera(camera_node); - render::scene::LightComponent light{}; - light.type = render::scene::LightType::Directional; - light.intensity = 4.0F; - scene.set_light(light_node, light); - - render::scene::RenderableComponent renderable{}; - renderable.vertex_buffer = vertex_buffer; - renderable.index_buffer = index_buffer; - renderable.index_count = static_cast(indices.size()); - renderable.program = program; - scene.set_renderable(mesh_node, renderable); - - scene.set_parent(camera_node, root, render::scene::ReparentPolicy::KeepLocalTransform); - scene.set_parent(light_node, root, render::scene::ReparentPolicy::KeepLocalTransform); - scene.set_parent(mesh_node, root, render::scene::ReparentPolicy::KeepLocalTransform); + constexpr std::uint32_t kInstanceRows = 12; + std::vector nodes; + nodes.reserve(kInstanceRows * kInstanceRows); + for (std::uint32_t y = 0; y < kInstanceRows; ++y) { + for (std::uint32_t x = 0; x < kInstanceRows; ++x) { + const auto node = scene.create_node("tri_instance"); + scene.set_parent(node, root, render::scene::ReparentPolicy::KeepLocalTransform); + render::core::Transform t{}; + t.translation = {static_cast(x) - 6.0F, static_cast(y) - 6.0F, 0.0F}; + t.scale = {0.15F, 0.15F, 0.15F}; + scene.set_local_transform(node, t); + render::scene::RenderableComponent rc{}; + rc.mesh_buffer = mesh_buffer; + rc.index_buffer = index_buffer; + rc.index_count = static_cast(indices.size()); + rc.material.program = program; + scene.set_renderable(node, rc); + nodes.push_back(node); + } + } while (!runtime.should_quit()) { runtime.begin_frame(); runtime.pump_events(); - const render::platform::WindowState& window = runtime.window_state(); - if (window.resized_this_frame) { - renderer.request_resize(window.width, window.height); - } - - const bool frame_started = renderer.begin_frame(); - if (!frame_started) { + const auto& window = runtime.window_state(); + if (window.resized_this_frame) renderer.request_resize(window.width, window.height); + if (!renderer.begin_frame()) { runtime.end_frame(); - render::platform::PlatformRuntime::sleep_for_milliseconds(1); continue; } @@ -183,49 +140,52 @@ int main(int argc, char** argv) { view.rect.width = static_cast(window.width); view.rect.height = static_cast(window.height); view.clear.rgba = 0x1f2233ff; - view.clear.clear_color = true; - view.clear.clear_depth = true; - view.clear.clear_stencil = false; - constexpr render::rendering::ViewId kMainView{0}; renderer.set_view(kMainView, view); shader_library.reload_if_stale(shader_id, program); - - if (program.idx != render::rendering::kInvalidHandle) { - renderable.program = program; - scene.set_renderable(mesh_node, renderable); - } - scene.update_world_transforms(); const float aspect_ratio = static_cast(window.width) / static_cast(window.height); - const std::optional camera_view = scene.build_camera_view(aspect_ratio); + const auto camera_view = scene.build_camera_view(aspect_ratio); if (camera_view.has_value()) { - const std::array view_matrix = to_array(camera_view->view); - const std::array projection_matrix = to_array(camera_view->projection); - renderer.set_view_transform(kMainView, - std::span{view_matrix}, - std::span{projection_matrix}); + const auto view_matrix = to_array(camera_view->view); + const auto projection_matrix = to_array(camera_view->projection); + renderer.set_view_transform(kMainView, std::span{view_matrix}, std::span{projection_matrix}); } - const std::vector draw_list = scene.collect_visible_renderables(); - for (const render::scene::VisibleRenderable& visible : draw_list) { - if (visible.renderable == nullptr || visible.world_transform == nullptr) { - continue; - } + std::vector submissions; + const auto visible = scene.collect_visible_renderables(); + 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()); + render::rendering::DrawSubmission draw{}; + draw.view = kMainView; + draw.mesh.vertex_buffer = vr.renderable->mesh_buffer; + draw.mesh.index_buffer = vr.renderable->index_buffer; + draw.mesh.index_count = vr.renderable->index_count; + draw.material = vr.renderable->material; + draw.draw_state = vr.renderable->draw_state; + draw.transform = world; + draw.sort_key = static_cast(vr.node.index); + submissions.push_back(draw); + } - const std::array world_matrix = to_array(visible.world_transform->to_matrix()); - render::rendering::MeshSubmission submission{}; - submission.mesh = render::rendering::MeshHandle{ - .vertex_buffer = visible.renderable->vertex_buffer, - .index_buffer = visible.renderable->index_buffer, - .index_count = visible.renderable->index_count, - }; - submission.program = visible.renderable->program; - submission.draw_state = visible.renderable->draw_state; - submission.transform = std::span{world_matrix}; - renderer.submit(kMainView, submission); + 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()); + } } renderer.end_frame(); @@ -233,10 +193,9 @@ int main(int argc, char** argv) { } renderer.destroy_program(program); - renderer.destroy_buffer(vertex_buffer); + renderer.destroy_buffer(mesh_buffer); renderer.destroy_buffer(index_buffer); renderer.shutdown(); runtime.shutdown(); - return 0; } diff --git a/tests/render/geometry_submission_tests.cpp b/tests/render/geometry_submission_tests.cpp new file mode 100644 index 0000000..0b5513f --- /dev/null +++ b/tests/render/geometry_submission_tests.cpp @@ -0,0 +1,51 @@ +#include "engine/render/buffer_types.hpp" +#include "engine/render/draw_submission.hpp" + +#include +#include +#include +#include +#include + +int main() { + using namespace render::rendering; + + VertexLayoutDescription layout{}; + layout.stride = 16; + layout.elements = { + {VertexAttribute::Position, 0, 3, AttributeType::Float, false, false}, + {VertexAttribute::Color0, 12, 4, AttributeType::Uint8, true, false}, + }; + const auto valid_layout = validate_vertex_layout(layout); + assert(valid_layout.valid); + + CpuMeshData mesh{}; + mesh.layout = layout; + mesh.vertex_count = 3; + mesh.vertex_data.resize(48); + mesh.index_type = IndexType::Uint16; + mesh.index_count = 3; + mesh.index_data.resize(6); + const auto valid_mesh = validate_cpu_mesh_data(mesh); + assert(valid_mesh.valid); + + std::array identity{}; + identity[0] = identity[5] = identity[10] = identity[15] = 1.0F; + + DrawSubmission draw{}; + draw.view = ViewId{0}; + draw.mesh.vertex_buffer = MeshBufferHandle{1}; + draw.mesh.index_buffer = IndexBufferHandle{2}; + draw.mesh.index_count = 3; + draw.material.program = ProgramHandle{3}; + draw.transform = identity; + + std::vector draws(8, draw); + SubmissionDiagnostics diagnostics{}; + const auto batches = build_draw_batches(draws, BatchPolicy{.min_instanced_count = 4, .max_instances_per_draw = 64}, &diagnostics); + assert(!batches.empty()); + assert(diagnostics.instanced_draws >= 1); + assert(diagnostics.submitted_instances == 8); + + return 0; +} diff --git a/tests/scene/scene_tests.cpp b/tests/scene/scene_tests.cpp index c2017cc..8729baa 100644 --- a/tests/scene/scene_tests.cpp +++ b/tests/scene/scene_tests.cpp @@ -57,6 +57,9 @@ int main() { assert(graph.set_light(child, light)); scene::RenderableComponent renderable{}; + renderable.mesh_buffer.idx = 7; + renderable.index_buffer.idx = 8; + renderable.material.program.idx = 11; renderable.index_count = 6; renderable.layer_mask = 0x1u; assert(graph.set_renderable(grandchild, renderable));