Files
EVEngine/test/indirect_builder.cpp
T
西风逍遥游 95b1491e86 feat(graphics): GPU-driven phase 1 - CPU-built indirect draws for opaque forward
- 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).
2026-08-20 21:30:54 +08:00

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);
}