fix: Shadow pass 接入 GPUDriven batch,实例上限提至 16384

100 背包掉帧 + 警告刷屏的根因:
1. backpack.obj 有 44 个 mesh 部件,100 实体 × 44 = 4400 实例
   打爆 4096 上限 → 每帧截断 + 告警刷屏。上限提至 16384
  (1MB/slot),告警改一次性
2. Shadow pass 仍逐实体逐部件画(4400 draw,CPU 提交瓶颈)。
   改为与 Forward 共用同一批 batch:Set0 绑 ObjectBuffer,
   lightVP 一次 push,44 draw 完成。shadow.vert 经
   gl_InstanceIndex 从 SSBO 取模型矩阵

实测(100 背包):GPU 总耗时 ~5.7ms(Shadow 2.9 + Forward 2.6 +
后处理 0.1),0 警告,帧率受 mailbox 垂直同步 pacing 限制
This commit is contained in:
2026-09-22 20:06:43 +08:00
parent 7fca721a24
commit 6a3fcac072
3 changed files with 50 additions and 38 deletions
+1 -1
View File
@@ -140,7 +140,7 @@ namespace Engine {
// GPUDriven:当帧实例数据(Render 期收集,Forward pass 消费)
std::vector<InstanceBatch> m_InstanceBatches;
std::vector<glm::mat4> m_InstanceModels;
static constexpr u32 kMaxInstances = 4096;
static constexpr u32 kMaxInstances = 16384;
// 可选叠加层(不拥有;Application 持有)
ImGuiLayer* m_ImGuiLayer = nullptr;
+41 -31
View File
@@ -52,10 +52,7 @@ namespace Engine
constexpr u32 kTextureSetIndex = 1;
constexpr u32 kShadowSetIndex = 2;
constexpr u32 kMaxLights = 4;
// Shadow pass:push constant = { lightVP, model }
constexpr u32 kShadowMapSize = 2048;
constexpr u32 kShadowPushSize = static_cast<u32>(2 * sizeof(glm::mat4));
// 全屏 pass 通用 push(16B):所有全屏 pass(Tonemap/Bloom 系)
// 统一声明与推送,规避 VUID-08878 的"range 定义必须一致"
@@ -111,9 +108,10 @@ namespace Engine
m_PipelineLayout = m_DescriptorManager->CreatePipelineLayout(
{m_UBOSetLayout, m_TextureSetLayout, m_ShadowSetLayout}, 0);
// Shadow pass 独立 layout:无 set,128B push constant
// Shadow pass layout:Set0 = UBO/ObjectBuffer(实例矩阵),push = lightVP
m_ShadowPipelineLayout = m_DescriptorManager->CreatePipelineLayout(
std::vector<vk::DescriptorSetLayout>{}, kShadowPushSize);
std::vector<vk::DescriptorSetLayout>{m_UBOSetLayout},
sizeof(glm::mat4));
// 7. ShaderManager(VK_EXT_shader_object)
m_ShaderManager = CreateScope<ShaderManager>(*m_Device);
@@ -135,10 +133,14 @@ namespace Engine
// shadow shader:depth-only,无 set
auto shadowVert = m_ShaderManager->CreateFromFile(
shaderDir + "shadow.vert.spv",
vk::ShaderStageFlagBits::eVertex, {}, kShadowPushSize);
vk::ShaderStageFlagBits::eVertex,
std::vector<vk::DescriptorSetLayout>{m_UBOSetLayout},
sizeof(glm::mat4));
auto shadowFrag = m_ShaderManager->CreateFromFile(
shaderDir + "shadow.frag.spv",
vk::ShaderStageFlagBits::eFragment, {}, kShadowPushSize);
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{m_UBOSetLayout},
sizeof(glm::mat4));
m_ShadowVertShader = shadowVert.Handle;
m_ShadowFragShader = shadowFrag.Handle;
@@ -225,7 +227,6 @@ namespace Engine
// 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>(
@@ -445,29 +446,22 @@ namespace Engine
vk::Rect2D scissor({0, 0}, {kShadowMapSize, kShadowMapSize});
cmd.setScissorWithCount(scissor);
// ---- 绘制:push {lightVP, model},与 Forward 用同一份 world 矩阵 ----
struct ShadowPush {
glm::mat4 lightVP;
glm::mat4 model;
};
// ---- 绘制:GPUDriven——与 Forward 共用同一批 batch ----
// lightVP 全 pass 只 push 一次;模型矩阵经实例索引从
// ObjectBuffer 取(与 Forward 同一份数据)
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_ShadowPipelineLayout, 0,
m_UBODescriptorSets[ctx.frame.frameSlot], {});
m_BoundScene->Each<TransformComponent, MeshRendererComponent>(
[&](Entity, TransformComponent& transform,
MeshRendererComponent& meshRenderer) {
Model* model = m_BoundAssets->GetModel(meshRenderer.model);
if (!model) return;
// stage 必须覆盖 layout range 声明的全部 stage(VUID-01796)
cmd.pushConstants(m_ShadowPipelineLayout,
vk::ShaderStageFlagBits::eVertex |
vk::ShaderStageFlagBits::eFragment,
0, sizeof(glm::mat4), &m_LightSpace);
const ShadowPush push{m_LightSpace, transform.world};
// stage 必须覆盖 layout range 声明的全部 stage(VUID-01796)
cmd.pushConstants(m_ShadowPipelineLayout,
vk::ShaderStageFlagBits::eVertex |
vk::ShaderStageFlagBits::eFragment,
0, kShadowPushSize, &push);
for (auto& mesh : model->GetMeshes()) {
mesh->Draw(cmd);
}
});
for (const auto& batch : m_InstanceBatches) {
batch.mesh->Draw(cmd, batch.count, batch.first);
}
cmd.endRendering();
})
@@ -848,6 +842,17 @@ namespace Engine
? static_cast<f32>(end - start) * m_TimestampPeriod / 1.0e6f
: 0.0f;
}
// 控制台遥测:每 120 次读取(≈60 帧 @2 slot)输出一行
static u32 telemetryCount = 0;
if (++telemetryCount % 120 == 1) {
std::string line = "GPU times:";
for (u32 i = 0; i < passCount; ++i) {
line += " " + m_PassNames[i] + "=" +
std::to_string(m_PassGpuMs[i]).substr(0, 5) + "ms";
}
ENGINE_INFO("{}", line);
}
}
void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets)
@@ -973,8 +978,13 @@ namespace Engine
}
}
if (instanceTruncated) {
ENGINE_WARN("GPUDriven: instance buffer full ({}), entities truncated",
kMaxInstances);
// 只告警一次(每帧告警会刷屏)
static bool warned = false;
if (!warned) {
warned = true;
ENGINE_WARN("GPUDriven: instance buffer full ({}), entities truncated",
kMaxInstances);
}
}
if (!m_InstanceModels.empty()) {
m_ObjectBuffer->Update(frameSlot, m_InstanceModels.data(),
+8 -6
View File
@@ -1,17 +1,19 @@
#version 450
// Shadow pass vertex:光源视角写深度。
// push constant = { lightVP, model },与 Forward pass 用同一份
// lightVP(UBO.lightSpace)和同一份 model(TransformComponent.world),
// 保证阴影贴图与主视角几何严格一致。
// GPUDriven:模型矩阵经 gl_InstanceIndex 从实例缓冲取(与 Forward
// 共用同一份数据),push 只带 lightVP(全 pass 一次)。
layout(location = 0) in vec3 inPosition;
layout(push_constant) uniform ShadowPush {
layout(std430, set = 0, binding = 1) readonly buffer ObjectBuffer {
mat4 models[];
} objects;
layout(push_constant) uniform PushConstants {
mat4 lightVP;
mat4 model;
} pc;
void main() {
gl_Position = pc.lightVP * pc.model * vec4(inPosition, 1.0);
gl_Position = pc.lightVP * objects.models[gl_InstanceIndex] * vec4(inPosition, 1.0);
}