- Shared GPU-driven types (GpuDrivenTypes.h) in the backend-independent graphics namespace; IndirectBuilder (pure CPU) sorts instances by (pipeline, material, mesh) and merges buckets into GpuIndirectCommand. - Base Graphics gains a capability-gated seam (supportsGpuDriven3D, gpuDrivenMeshRecord/MaterialRecord, gpuDrivenSubmitOpaque); WebGPU keeps the default false. - Vulkan backend: mesh3d_gpudriven vert/frag (instance + bindless textures + material table, mirroring mesh3d PBR/CSM/IBL shading), a dedicated pipeline with a firstInstance push constant, per-frame arena upload of the sorted instance buffer + commands, and per-bucket vkCmdDrawIndexedIndirect. - Bindless set1 uses fixed-size descriptor arrays + dynamic indexing instead of nonuniform() because this toolchain's glslc lacks GL_EXT_nonuniform_qualifier; indices are uniform per bucket (one material per indirect draw), which is spec-legal. Caps requirements updated to match (drops runtime descriptor arrays / non-uniform indexing). - RenderControl feature 'gpuDriven' (default OFF; opt-in until runtime verified). RenderSystem3D collects opaque instances when enabled and eligible (default camera, Material-driven, no custom shader, no clustered); any ineligible item falls back to the legacy per-draw path. - Tests: IndirectBuilder merging/stable-order/pipeline-separation (CPU). GPU-driven runtime tests (GpuDriven.*) remain pending driver recovery. Rendering is unchanged by default (feature off).
95 lines
2.6 KiB
C++
95 lines
2.6 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "graphics/GpuDrivenTypes.h"
|
|
#include "graphics/IndirectBuilder.h"
|
|
|
|
using namespace eve::graphics;
|
|
|
|
namespace {
|
|
|
|
GpuMeshRecord makeMeshRecord(uint32_t indexCount) {
|
|
GpuMeshRecord rec{};
|
|
rec.indexCount = indexCount;
|
|
rec.firstIndex = 0;
|
|
rec.vertexBase = 0;
|
|
rec.indexType = 1;
|
|
return rec;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("IndirectBuilder.mergesBuckets") {
|
|
std::vector<GpuMeshRecord> table;
|
|
table.push_back(makeMeshRecord(36)); // mesh 0
|
|
table.push_back(makeMeshRecord(72)); // mesh 1
|
|
|
|
IndirectBuilder b;
|
|
b.add(0, 0, 0, 0); // seq, mesh, material, pipeline
|
|
b.add(1, 0, 0, 0);
|
|
b.add(2, 1, 0, 0);
|
|
b.add(3, 0, 1, 0);
|
|
REQUIRE(b.build(table) == 3);
|
|
|
|
const auto &cmds = b.commands();
|
|
REQUIRE(cmds.size() == 3);
|
|
CHECK(cmds[0].instanceCount == 2); // material 0 + mesh 0
|
|
CHECK(cmds[0].firstInstance == 0);
|
|
CHECK(cmds[0].indexCount == 36);
|
|
CHECK(cmds[1].instanceCount == 1); // material 0 + mesh 1
|
|
CHECK(cmds[1].firstInstance == 2);
|
|
CHECK(cmds[1].indexCount == 72);
|
|
CHECK(cmds[2].instanceCount == 1); // material 1 + mesh 0
|
|
CHECK(cmds[2].firstInstance == 3);
|
|
|
|
const auto &order = b.sortedInstanceOrder();
|
|
REQUIRE(order.size() == 4);
|
|
CHECK(order[0] == 0);
|
|
CHECK(order[1] == 1);
|
|
CHECK(order[2] == 2);
|
|
CHECK(order[3] == 3);
|
|
}
|
|
|
|
TEST_CASE("IndirectBuilder.stableOrderWithinBucket") {
|
|
std::vector<GpuMeshRecord> table;
|
|
table.push_back(makeMeshRecord(12));
|
|
|
|
IndirectBuilder b;
|
|
b.add(7, 0, 5, 0);
|
|
b.add(3, 0, 5, 0);
|
|
b.add(9, 0, 5, 0);
|
|
REQUIRE(b.build(table) == 1);
|
|
CHECK(b.commands()[0].instanceCount == 3);
|
|
|
|
// Stable: sequence numbers inside the bucket drive the tiebreak (7, 3, 9 → 3, 7, 9).
|
|
const auto &order = b.sortedInstanceOrder();
|
|
REQUIRE(order.size() == 3);
|
|
CHECK(order[0] == 1);
|
|
CHECK(order[1] == 0);
|
|
CHECK(order[2] == 2);
|
|
}
|
|
|
|
TEST_CASE("IndirectBuilder.pipelineSeparatesBuckets") {
|
|
std::vector<GpuMeshRecord> table;
|
|
table.push_back(makeMeshRecord(12));
|
|
|
|
IndirectBuilder b;
|
|
b.add(0, 0, 0, 1);
|
|
b.add(1, 0, 0, 2);
|
|
REQUIRE(b.build(table) == 2); // same mesh+material, different pipeline
|
|
CHECK(b.commands()[0].instanceCount == 1);
|
|
CHECK(b.commands()[1].instanceCount == 1);
|
|
}
|
|
|
|
TEST_CASE("IndirectBuilder.skipsUnknownMesh") {
|
|
std::vector<GpuMeshRecord> table;
|
|
table.push_back(makeMeshRecord(12));
|
|
|
|
IndirectBuilder b;
|
|
b.add(0, 0, 0, 0);
|
|
b.add(1, 99, 0, 0); // meshId outside the table
|
|
REQUIRE(b.build(table) == 1);
|
|
CHECK(b.commands()[0].instanceCount == 1);
|
|
CHECK(b.commands()[0].firstInstance == 0);
|
|
}
|