feat: GPUDriven 实例化渲染——draw 数与实体数解耦

- UniformBuffer ring 支持 eStorageBuffer usage(GPUDriven 实例矩阵
  per-frame-slot,正确性保证与 UBO 一致:fence 等待后写入)
- DescriptorManager 加 UpdateStorageBuffer;池补 STORAGE_BUFFER 类型
- Set0 新增 binding1 = 实例矩阵 SSBO(vertex 读取,std430)
- Forward pass 重写:每帧从 ECS 收集实例矩阵(按 mesh 聚合 batch,
  两趟遍历保证 batch 区间连续),同 mesh 的 N 个实体合并为
  一次 drawIndexed(instanceCount=N),firstInstance 指向 batch 起点;
  vertex shader 经 gl_InstanceIndex 取模型矩阵,forward 不再用 push
- draw 数 = 唯一 mesh 数,与实体数量解耦(4096 实例上限,
  超出截断告警;聚合改哈希为后续 TODO)

验证:8 pass 全活,validation 0 error 0 warning,GPU Timings 面板
实测各 pass 毫秒,场景渲染与阴影正确
This commit is contained in:
2026-09-22 19:46:17 +08:00
parent 41e4cc85d5
commit 7fca721a24
10 changed files with 138 additions and 50 deletions
+3 -1
View File
@@ -30,7 +30,9 @@ namespace Engine
Mesh(const Mesh&) = delete;
Mesh& operator=(const Mesh&) = delete;
void Draw(vk::CommandBuffer cmd) const;
// instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后,
// 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1, u32 firstInstance = 0) const;
Material* GetMaterial() const { return m_Material.get(); }
u32 GetIndexCount() const { return m_IndexCount; }
+14
View File
@@ -31,6 +31,14 @@ namespace Engine {
namespace ECS { class Scene; }
namespace Assets { class AssetManager; }
class ImGuiLayer;
class Mesh;
// GPUDriven:按 mesh 聚合的绘制批
struct InstanceBatch {
Mesh* mesh;
u32 first; // 在实例矩阵数组中的起始下标
u32 count; // 实例数
};
// Renderer 职责(收敛后):
// 1. 装配底层模块(Device/Swapchain/Sync/Command/Descriptor/Shader)
@@ -78,6 +86,7 @@ namespace Engine {
Scope<SyncManager> m_SyncManager;
Scope<CommandManager> m_CommandManager;
Scope<UniformBuffer> m_UniformBuffer;
Scope<UniformBuffer> m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO)
Scope<RG::RenderGraph> m_RenderGraph;
// ---- 管线装配(shader object 方案,无 vk::Pipeline)----
@@ -128,6 +137,11 @@ namespace Engine {
Assets::AssetManager* m_BoundAssets = nullptr;
glm::mat4 m_LightSpace{1.0f}; // 阴影光源 VP(每帧由 Render 算好)
// GPUDriven:当帧实例数据(Render 期收集,Forward pass 消费)
std::vector<InstanceBatch> m_InstanceBatches;
std::vector<glm::mat4> m_InstanceModels;
static constexpr u32 kMaxInstances = 4096;
// 可选叠加层(不拥有;Application 持有)
ImGuiLayer* m_ImGuiLayer = nullptr;
@@ -34,6 +34,8 @@ namespace Engine {
void UpdateUniformBuffer(vk::DescriptorSet set, u32 binding,
vk::Buffer buffer, u64 size);
void UpdateStorageBuffer(vk::DescriptorSet set, u32 binding,
vk::Buffer buffer, u64 size);
void UpdateTexture(vk::DescriptorSet set, u32 binding,
vk::Sampler sampler, vk::ImageView view);
+4 -1
View File
@@ -25,8 +25,11 @@ namespace Engine {
// 接口已按"写入哪个槽位"设计,届时只是内部实现替换。
class UniformBuffer {
public:
// usage:默认 eUniformBuffer;GPUDriven 实例数据传 eStorageBuffer
//(ring 化的正确性保证对所有 per-frame 数据一致)
UniformBuffer(Device& device, MemoryAllocator& allocator,
u64 size, u32 frameCount);
u64 size, u32 frameCount,
vk::BufferUsageFlags usage = vk::BufferUsageFlagBits::eUniformBuffer);
~UniformBuffer() = default;
UniformBuffer(const UniformBuffer&) = delete;
+2 -2
View File
@@ -55,10 +55,10 @@ namespace Engine
STATS.RemoveMesh(m_VertexBytes, m_IndexBytes);
}
void Mesh::Draw(vk::CommandBuffer cmd) const
void Mesh::Draw(vk::CommandBuffer cmd, u32 instanceCount, u32 firstInstance) const
{
cmd.bindVertexBuffers(0, m_VertexBuffer->Get(), {0});
cmd.bindIndexBuffer(m_IndexBuffer->Get(), 0, vk::IndexType::eUint32);
cmd.drawIndexed(m_IndexCount, 1, 0, 0, 0);
cmd.drawIndexed(m_IndexCount, instanceCount, 0, 0, firstInstance);
}
} // namespace Engine
+80 -30
View File
@@ -47,6 +47,7 @@ namespace Engine
};
constexpr u32 kUboBinding = 0;
constexpr u32 kObjectBinding = 1;
constexpr u32 kUBOSetIndex = 0;
constexpr u32 kTextureSetIndex = 1;
constexpr u32 kShadowSetIndex = 2;
@@ -85,10 +86,12 @@ namespace Engine
// 3. DescriptorManager
m_DescriptorManager = CreateScope<DescriptorManager>(*m_Device);
// 4. Set 0: UBO
// 4. Set 0: UBO + GPUDriven 实例数据(SSBO,vertex 读取)
std::vector<DescriptorBinding> uboBindings = {
{kUboBinding, vk::DescriptorType::eUniformBuffer,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{kObjectBinding, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
};
m_UBOSetLayout = m_DescriptorManager->CreateLayout(uboBindings);
@@ -104,10 +107,9 @@ namespace Engine
};
m_ShadowSetLayout = m_DescriptorManager->CreateLayout(shadowBindings);
// 6. PipelineLayout:三个 set;push constant 上限 128B
//(Shadow pass 的 {lightVP, model} 用满;forward shader block 只用前 64B)
// 6. PipelineLayout:三个 set;forward 无 push(GPUDriven 经 SSBO)
m_PipelineLayout = m_DescriptorManager->CreatePipelineLayout(
{m_UBOSetLayout, m_TextureSetLayout, m_ShadowSetLayout}, kShadowPushSize);
{m_UBOSetLayout, m_TextureSetLayout, m_ShadowSetLayout}, 0);
// Shadow pass 独立 layout:无 set,128B push constant
m_ShadowPipelineLayout = m_DescriptorManager->CreatePipelineLayout(
@@ -117,14 +119,15 @@ namespace Engine
m_ShaderManager = CreateScope<ShaderManager>(*m_Device);
std::string shaderDir = "shaders/";
// forward vert/frag 均无 push(range 定义一致:都没有,VUID-08878)
auto vert = m_ShaderManager->CreateFromFile(
shaderDir + "simple_shader.vert.spv",
vk::ShaderStageFlagBits::eVertex,
{m_UBOSetLayout, m_TextureSetLayout, m_ShadowSetLayout}, kShadowPushSize);
{m_UBOSetLayout, m_TextureSetLayout, m_ShadowSetLayout}, 0);
auto frag = m_ShaderManager->CreateFromFile(
shaderDir + "simple_shader.frag.spv",
vk::ShaderStageFlagBits::eFragment,
{m_UBOSetLayout, m_TextureSetLayout, m_ShadowSetLayout}, kShadowPushSize);
{m_UBOSetLayout, m_TextureSetLayout, m_ShadowSetLayout}, 0);
m_VertShader = vert.Handle;
m_FragShader = frag.Handle;
@@ -220,9 +223,15 @@ namespace Engine
*m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount());
m_CommandManager = CreateScope<CommandManager>(*m_Device, kMaxFramesInFlight);
// 10. Per-frame UBO ring + 每槽位的 UBO descriptor set
// 10. Per-frame UBO ring + GPUDriven 实例 SSBO ring
// (同一 ring 类,SSBO 只是 usage 不同;正确性保证一致)
constexpr u32 kMaxInstances = 4096;
m_UniformBuffer = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator, sizeof(UniformBufferObject), kMaxFramesInFlight);
m_ObjectBuffer = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
m_UBODescriptorSets.resize(kMaxFramesInFlight);
m_ShadowDescriptorSets.resize(kMaxFramesInFlight);
@@ -236,6 +245,9 @@ namespace Engine
m_DescriptorManager->UpdateUniformBuffer(
m_UBODescriptorSets[slot], kUboBinding,
m_UniformBuffer->Get(slot), m_UniformBuffer->GetSize());
m_DescriptorManager->UpdateStorageBuffer(
m_UBODescriptorSets[slot], kObjectBinding,
m_ObjectBuffer->Get(slot), m_ObjectBuffer->GetSize());
m_ShadowDescriptorSets[slot] =
m_DescriptorManager->AllocateDescriptorSet(m_ShadowSetLayout);
@@ -564,29 +576,16 @@ namespace Engine
vk::ColorComponentFlagBits::eA;
cmd.setColorWriteMaskEXT(0, colorWriteMask);
// ---- 绘制:遍历 ECS 渲染体 ----
// Each 以 Transform/MeshRenderer 中较小的池为主表——
// 大场景下遍历开销与最小池规模成正比
m_BoundScene->Each<TransformComponent, MeshRendererComponent>(
[&](Entity, TransformComponent& transform,
MeshRendererComponent& meshRenderer) {
// 句柄 → 资产(悬空句柄会被 AssetCache FATAL 拦截)
Model* model = m_BoundAssets->GetModel(meshRenderer.model);
if (!model) return;
// stage 必须覆盖 layout range 声明的全部 stage(VUID-01796)
cmd.pushConstants(m_PipelineLayout,
vk::ShaderStageFlagBits::eVertex |
vk::ShaderStageFlagBits::eFragment,
0, sizeof(glm::mat4), &transform.world);
for (auto& mesh : model->GetMeshes()) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_PipelineLayout, kTextureSetIndex,
mesh->GetMaterial()->GetDescriptorSet(), {});
mesh->Draw(cmd);
}
});
// ---- 绘制:GPUDriven 按 mesh 分 batch ----
// 实例矩阵已在 Render() 里写入 ObjectBuffer(per-slot SSBO);
// 同 mesh 的 N 个实体合并为一次 drawIndexed(instanceCount=N),
// draw 数 = 唯一 mesh 数,与实体数量解耦
for (const auto& batch : m_InstanceBatches) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_PipelineLayout, kTextureSetIndex,
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
batch.mesh->Draw(cmd, batch.count, batch.first);
}
cmd.endRendering();
})
@@ -931,6 +930,57 @@ namespace Engine
m_UniformBuffer->Update(frameSlot, &ubo, sizeof(ubo));
m_SyncManager->ResetFence(frameSlot);
// ---- GPUDriven:收集实例数据(按 mesh 聚合 batch)----
// 两趟遍历:先收集唯一 mesh(保持首现序),再逐 batch 追加实例矩阵,
// 保证每个 batch 的矩阵区间连续(draw 用 firstInstance 指向起点)。
// TODO: 实体量大时改哈希聚合 + 单趟填充
m_InstanceBatches.clear();
m_InstanceModels.clear();
bool instanceTruncated = false;
std::vector<Mesh*> uniqueMeshes;
scene.Each<TransformComponent, MeshRendererComponent>(
[&](Entity, TransformComponent&, MeshRendererComponent& mr) {
Model* model = assets.GetModel(mr.model);
if (!model) return;
for (auto& mesh : model->GetMeshes()) {
if (std::find(uniqueMeshes.begin(), uniqueMeshes.end(),
mesh.get()) == uniqueMeshes.end()) {
uniqueMeshes.push_back(mesh.get());
}
}
});
for (Mesh* meshPtr : uniqueMeshes) {
InstanceBatch batch{meshPtr,
static_cast<u32>(m_InstanceModels.size()), 0};
scene.Each<TransformComponent, MeshRendererComponent>(
[&](Entity, TransformComponent& t, MeshRendererComponent& mr) {
Model* model = assets.GetModel(mr.model);
if (!model) return;
for (auto& m : model->GetMeshes()) {
if (m.get() != meshPtr) continue;
if (m_InstanceModels.size() >= kMaxInstances) {
instanceTruncated = true;
return;
}
m_InstanceModels.push_back(t.world);
++batch.count;
}
});
if (batch.count > 0) {
m_InstanceBatches.push_back(batch);
}
}
if (instanceTruncated) {
ENGINE_WARN("GPUDriven: instance buffer full ({}), entities truncated",
kMaxInstances);
}
if (!m_InstanceModels.empty()) {
m_ObjectBuffer->Update(frameSlot, m_InstanceModels.data(),
m_InstanceModels.size() * sizeof(glm::mat4));
}
// 2. acquire swapchain image
u32 imageIndex = 0;
try {
+19 -1
View File
@@ -11,9 +11,10 @@ namespace Engine
DescriptorManager::DescriptorManager(Device& device)
: m_Device(device)
{
std::array<vk::DescriptorPoolSize, 2> poolSizes = {
std::array<vk::DescriptorPoolSize, 3> poolSizes = {
vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, kMaxSets),
vk::DescriptorPoolSize(vk::DescriptorType::eCombinedImageSampler, kMaxSets),
vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, kMaxSets),
};
vk::DescriptorPoolCreateInfo poolInfo;
@@ -76,6 +77,23 @@ namespace Engine
m_Device.GetDevice().updateDescriptorSets(write, {});
}
void DescriptorManager::UpdateStorageBuffer(vk::DescriptorSet set, u32 binding,
vk::Buffer buffer, u64 size)
{
vk::DescriptorBufferInfo bufferInfo;
bufferInfo.setBuffer(buffer).setOffset(0).setRange(size);
vk::WriteDescriptorSet write;
write.setDstSet(set)
.setDstBinding(binding)
.setDstArrayElement(0)
.setDescriptorType(vk::DescriptorType::eStorageBuffer)
.setDescriptorCount(1)
.setBufferInfo(bufferInfo);
m_Device.GetDevice().updateDescriptorSets(write, {});
}
void DescriptorManager::UpdateTexture(vk::DescriptorSet set, u32 binding,
vk::Sampler sampler, vk::ImageView view)
{
+4 -4
View File
@@ -10,15 +10,15 @@
namespace Engine {
UniformBuffer::UniformBuffer(Device& device, MemoryAllocator& allocator,
u64 size, u32 frameCount)
u64 size, u32 frameCount,
vk::BufferUsageFlags usage)
: m_Size(size)
{
m_Buffers.reserve(frameCount);
for (u32 i = 0; i < frameCount; ++i) {
// Uniform Buffer 需要 CPU 每帧更新,所以用 HOST_VISIBLE | HOST_COHERENT
// CPU 每帧更新,所以用 HOST_VISIBLE | HOST_COHERENT
m_Buffers.push_back(CreateScope<Buffer>(
device, allocator, size,
vk::BufferUsageFlagBits::eUniformBuffer,
device, allocator, size, usage,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent));
}
-6
View File
@@ -1,6 +0,0 @@
//
// Created by atdunbg on 2026/9/20.
//
#define STB_IMAGE_IMPLEMENTATION
#include <stb/stb_image.h>