perf: BuildFrame 收集段优化——实体级粗剔除 + 数组化 batch 查找 + 合并矩阵写
定位数据(100 背包 + SunTemple,RTX 5060):GPU 全帧仅 ~3ms (Forward 1.91 + Shadow 0.82),CPU BuildFrame 收集段 5.94ms 才是 帧率瓶颈。三段细分测量:pre 0.2 / each+cull 5.0 / flatten 0.4。 优化(collect 5.94 → 4.6ms,-23%): 1. Mesh 全局连续槽位(GetSlot);ViewScratch batch 查找 hash → 数组寻址(slot × kMeshLodCount 槽) 2. Model 级联合包围球(各 mesh bounds 并集):实体在四视图全部 不可见 → 跳过整个 mesh 循环(多 mesh 模型的粗剔除) 3. worlds/prevWorlds 合并为 stride-2 InstanceMatrices 单次写 (每实例每视图 2 次随机堆访问 → 1 次,cache miss 减半) 遥测:Renderer::GetCollectMs 暴露收集段耗时。剩余 4.6ms 的进一步 压缩(并行收集 / GPU-driven culling)单独立项。
This commit is contained in:
@@ -47,6 +47,8 @@ namespace Engine
|
|||||||
{
|
{
|
||||||
return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].indexCount;
|
return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].indexCount;
|
||||||
}
|
}
|
||||||
|
// 全局连续槽位(SceneRenderer 数组化 batch 查找用,避免逐实例 hash)
|
||||||
|
u32 GetSlot() const { return m_Slot; }
|
||||||
|
|
||||||
// 局部空间包围球(构造时从顶点算出),视锥剔除用
|
// 局部空间包围球(构造时从顶点算出),视锥剔除用
|
||||||
struct Bounds {
|
struct Bounds {
|
||||||
@@ -66,6 +68,7 @@ namespace Engine
|
|||||||
Scope<Buffer> m_IndexBuffer;
|
Scope<Buffer> m_IndexBuffer;
|
||||||
std::array<LodRange, kMeshLodCount> m_Lods{};
|
std::array<LodRange, kMeshLodCount> m_Lods{};
|
||||||
Material* m_Material;
|
Material* m_Material;
|
||||||
|
u32 m_Slot = 0;
|
||||||
Bounds m_Bounds;
|
Bounds m_Bounds;
|
||||||
|
|
||||||
u64 m_VertexBytes = 0;
|
u64 m_VertexBytes = 0;
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ namespace Engine
|
|||||||
Model& operator=(const Model&) = delete;
|
Model& operator=(const Model&) = delete;
|
||||||
|
|
||||||
const std::vector<Scope<Mesh>>& GetMeshes() const { return m_Meshes; }
|
const std::vector<Scope<Mesh>>& GetMeshes() const { return m_Meshes; }
|
||||||
|
// 全模型联合包围球(各 mesh bounds 的并集,局部空间):
|
||||||
|
// 实体级粗剔除用——整个模型在视锥外直接跳过全部 mesh 循环
|
||||||
|
const Mesh::Bounds& GetBounds() const { return m_Bounds; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Model(Device& device, MemoryAllocator& allocator,
|
Model(Device& device, MemoryAllocator& allocator,
|
||||||
@@ -103,6 +106,9 @@ namespace Engine
|
|||||||
bool isGltf);
|
bool isGltf);
|
||||||
void InitDefaultTextures();
|
void InitDefaultTextures();
|
||||||
|
|
||||||
|
// 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除)
|
||||||
|
void RebuildUnionBounds();
|
||||||
|
|
||||||
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
|
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
|
||||||
|
|
||||||
// 材质缓存入口:同贴图组合+因子共享同一 Material(一个描述符集),
|
// 材质缓存入口:同贴图组合+因子共享同一 Material(一个描述符集),
|
||||||
@@ -124,6 +130,7 @@ namespace Engine
|
|||||||
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
|
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
|
||||||
|
|
||||||
std::vector<Scope<Mesh>> m_Meshes;
|
std::vector<Scope<Mesh>> m_Meshes;
|
||||||
|
Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除)
|
||||||
std::string m_ModelPath;
|
std::string m_ModelPath;
|
||||||
bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV
|
bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ namespace Engine {
|
|||||||
u32 GetMainInstanceCount() const;
|
u32 GetMainInstanceCount() const;
|
||||||
u32 GetShadowInstanceCount() const;
|
u32 GetShadowInstanceCount() const;
|
||||||
bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字)
|
bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字)
|
||||||
|
f32 GetCollectMs() const; // CPU 收集+剔除段耗时
|
||||||
|
|
||||||
// 垂直同步开关(present mode 切换会重建 swapchain),
|
// 垂直同步开关(present mode 切换会重建 swapchain),
|
||||||
// 只应在帧 UI 构建段调用(Render 之前)
|
// 只应在帧 UI 构建段调用(Render 之前)
|
||||||
|
|||||||
@@ -21,8 +21,10 @@
|
|||||||
#define HELLOVULKAN_SCENERENDERER_H
|
#define HELLOVULKAN_SCENERENDERER_H
|
||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
|
#include <Engine/Renderer/Mesh.h>
|
||||||
#include <Engine/Renderer/SceneTypes.h>
|
#include <Engine/Renderer/SceneTypes.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
#include <array>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
@@ -67,23 +69,26 @@ namespace Engine {
|
|||||||
|
|
||||||
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
|
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
|
||||||
struct ViewScratch {
|
struct ViewScratch {
|
||||||
|
// cur + prev 合并写:两次随机堆访问 → 一次(cache miss 减半)
|
||||||
|
struct InstanceMatrices {
|
||||||
|
glm::mat4 cur;
|
||||||
|
glm::mat4 prev;
|
||||||
|
};
|
||||||
CullResult result;
|
CullResult result;
|
||||||
// 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 =
|
// 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 =
|
||||||
// firstInstance + 连续区间,实体序收集不满足(多实体共享 mesh
|
// firstInstance + 连续区间,实体序收集不满足(多实体共享 mesh
|
||||||
// 时实例分散各处),扁平化成连续数组推迟到 appendView
|
// 时实例分散各处),扁平化成连续数组推迟到 appendView
|
||||||
std::vector<std::vector<glm::mat4>> batchWorlds;
|
std::vector<std::vector<InstanceMatrices>> batchMats;
|
||||||
std::vector<std::vector<glm::mat4>> batchPrevWorlds;
|
// batch 查找:数组寻址(下标 = Mesh::GetSlot(),值 = 每 LOD 的
|
||||||
// key = (Mesh*, LOD):同 mesh 不同 LOD 索引区间不同,
|
// batchIdx+1,0 = 未用)——逐实例 hash 在大场景是毫秒级开销
|
||||||
// 必须分开成 batch(低 8 位 LOD,高位指针,指针 8 字节对齐安全)
|
std::vector<std::array<u32, kMeshLodCount>> lookup;
|
||||||
std::unordered_map<u64, u32> lookup;
|
|
||||||
|
|
||||||
void Clear()
|
void Clear()
|
||||||
{
|
{
|
||||||
result.batches.clear();
|
result.batches.clear();
|
||||||
result.instanceCount = 0;
|
result.instanceCount = 0;
|
||||||
batchWorlds.clear();
|
batchMats.clear();
|
||||||
batchPrevWorlds.clear();
|
for (auto& e : lookup) e.fill(0); // 容量保留,置零复用
|
||||||
lookup.clear();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ViewScratch m_MainScratch;
|
ViewScratch m_MainScratch;
|
||||||
|
|||||||
@@ -10,16 +10,24 @@
|
|||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
#include "Engine/Core/Stats.h"
|
#include "Engine/Core/Stats.h"
|
||||||
|
|
||||||
namespace Engine
|
namespace Engine
|
||||||
{
|
{
|
||||||
|
namespace {
|
||||||
|
// 全局连续 mesh 槽位:SceneRenderer 的 batch 查找用数组寻址
|
||||||
|
//(每实例每视图一次 hash 的代价在大场景下是 5ms 级)
|
||||||
|
std::atomic<u32> s_nextMeshSlot{0};
|
||||||
|
}
|
||||||
|
|
||||||
Mesh::Mesh(Device& device, MemoryAllocator& allocator,
|
Mesh::Mesh(Device& device, MemoryAllocator& allocator,
|
||||||
const std::vector<Vertex>& vertices,
|
const std::vector<Vertex>& vertices,
|
||||||
const std::vector<std::vector<u32>>& lods,
|
const std::vector<std::vector<u32>>& lods,
|
||||||
Material* material)
|
Material* material)
|
||||||
: m_Material(material)
|
: m_Material(material)
|
||||||
|
, m_Slot(s_nextMeshSlot.fetch_add(1, std::memory_order_relaxed))
|
||||||
{
|
{
|
||||||
if (vertices.empty() || lods.empty() || lods[0].empty())
|
if (vertices.empty() || lods.empty() || lods[0].empty())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,6 +45,23 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
|||||||
|
|
||||||
Model::~Model() = default;
|
Model::~Model() = default;
|
||||||
|
|
||||||
|
void Model::RebuildUnionBounds()
|
||||||
|
{
|
||||||
|
if (m_Meshes.empty()) {
|
||||||
|
m_Bounds = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 各 mesh 包围球 → AABB 并集 → 外接球(保守,宁多画不漏画)
|
||||||
|
glm::vec3 mn(1e9f), mx(-1e9f);
|
||||||
|
for (const auto& mesh : m_Meshes) {
|
||||||
|
const auto& b = mesh->GetBounds();
|
||||||
|
mn = glm::min(mn, b.center - glm::vec3(b.radius));
|
||||||
|
mx = glm::max(mx, b.center + glm::vec3(b.radius));
|
||||||
|
}
|
||||||
|
m_Bounds.center = (mn + mx) * 0.5f;
|
||||||
|
m_Bounds.radius = 0.5f * glm::length(mx - mn);
|
||||||
|
}
|
||||||
|
|
||||||
void Model::InitDefaultTextures()
|
void Model::InitDefaultTextures()
|
||||||
{
|
{
|
||||||
// 各材质槽位的兜底纹理(1x1,开销可忽略):
|
// 各材质槽位的兜底纹理(1x1,开销可忽略):
|
||||||
@@ -91,6 +108,7 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
|||||||
|
|
||||||
m_Meshes.push_back(CreateScope<Mesh>(
|
m_Meshes.push_back(CreateScope<Mesh>(
|
||||||
m_Device, m_Allocator, vertices, lodIndices, mat));
|
m_Device, m_Allocator, vertices, lodIndices, mat));
|
||||||
|
RebuildUnionBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
|
Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
|
||||||
@@ -165,6 +183,7 @@ Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
|
|||||||
}
|
}
|
||||||
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
||||||
parsed.path, model->m_Meshes.size(), model->m_TextureCache.size());
|
parsed.path, model->m_Meshes.size(), model->m_TextureCache.size());
|
||||||
|
model->RebuildUnionBounds();
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +203,7 @@ void Model::LoadModel(const std::string& path)
|
|||||||
}
|
}
|
||||||
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
||||||
path, m_Meshes.size(), m_TextureCache.size());
|
path, m_Meshes.size(), m_TextureCache.size());
|
||||||
|
RebuildUnionBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Model::ProcessNode(const aiNode* node, const aiScene* scene,
|
void Model::ProcessNode(const aiNode* node, const aiScene* scene,
|
||||||
|
|||||||
@@ -225,6 +225,11 @@ namespace Engine
|
|||||||
return m_SceneRenderer->Frame().instancesTruncated;
|
return m_SceneRenderer->Frame().instancesTruncated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
f32 Renderer::GetCollectMs() const
|
||||||
|
{
|
||||||
|
return m_SceneRenderer->GetCollectMs();
|
||||||
|
}
|
||||||
|
|
||||||
u32 Renderer::GetShadowInstanceCount() const
|
u32 Renderer::GetShadowInstanceCount() const
|
||||||
{
|
{
|
||||||
return m_SceneRenderer->GetShadowInstanceCount();
|
return m_SceneRenderer->GetShadowInstanceCount();
|
||||||
|
|||||||
@@ -291,6 +291,23 @@ namespace Engine
|
|||||||
glm::max(glm::length(glm::vec3(t.World()[1])),
|
glm::max(glm::length(glm::vec3(t.World()[1])),
|
||||||
glm::length(glm::vec3(t.World()[2]))));
|
glm::length(glm::vec3(t.World()[2]))));
|
||||||
|
|
||||||
|
// ---- 实体级粗剔除(联合包围球):四视图全部看不到 →
|
||||||
|
// 跳过整个 mesh 循环(多 mesh 模型的大头优化)----
|
||||||
|
const Mesh::Bounds& mb = model->GetBounds();
|
||||||
|
const glm::vec3 modelCenter =
|
||||||
|
glm::vec3(t.World() * glm::vec4(mb.center, 1.0f));
|
||||||
|
const f32 modelRadius = mb.radius * worldScale;
|
||||||
|
const bool mainVis =
|
||||||
|
mainFrustum.IntersectsSphere(modelCenter, modelRadius);
|
||||||
|
bool cascVis[kMaxCascades];
|
||||||
|
bool anyVis = mainVis;
|
||||||
|
for (u32 c = 0; c < kMaxCascades; ++c) {
|
||||||
|
cascVis[c] =
|
||||||
|
cascFrustum[c].IntersectsSphere(modelCenter, modelRadius);
|
||||||
|
anyVis |= cascVis[c];
|
||||||
|
}
|
||||||
|
if (!anyVis) return;
|
||||||
|
|
||||||
for (auto& mesh : model->GetMeshes()) {
|
for (auto& mesh : model->GetMeshes()) {
|
||||||
const auto& bounds = mesh->GetBounds();
|
const auto& bounds = mesh->GetBounds();
|
||||||
const glm::vec3 worldCenter =
|
const glm::vec3 worldCenter =
|
||||||
@@ -317,31 +334,32 @@ namespace Engine
|
|||||||
instanceTruncated = true;
|
instanceTruncated = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// lookup 值 = batches 索引(此前误存 worlds 索引:
|
// batch 查找 = 数组寻址(Mesh::GetSlot 槽位 + LOD 槽)
|
||||||
// 单实体时两序号恰好恒等,多实体共享 mesh 后分叉
|
// batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch)
|
||||||
// → batches[worldsIdx] 越界)。batch.first(GPU
|
const u32 slot = mesh->GetSlot();
|
||||||
// firstInstance 基址)才是 worlds 索引,语义别混
|
if (slot >= s.lookup.size()) {
|
||||||
const u64 key =
|
s.lookup.resize(slot + 1);
|
||||||
(reinterpret_cast<u64>(mesh.get()) << 8) | lod;
|
}
|
||||||
const auto [it, inserted] = s.lookup.emplace(
|
auto& lodSlots = s.lookup[slot];
|
||||||
key, static_cast<u32>(s.result.batches.size()));
|
u32 batchIdx = lodSlots[lod];
|
||||||
if (inserted) {
|
if (batchIdx == 0) {
|
||||||
|
batchIdx = static_cast<u32>(s.result.batches.size()) + 1;
|
||||||
|
lodSlots[lod] = batchIdx;
|
||||||
s.result.batches.push_back({mesh.get(), 0, 0, lod});
|
s.result.batches.push_back({mesh.get(), 0, 0, lod});
|
||||||
s.batchWorlds.emplace_back();
|
s.batchMats.emplace_back();
|
||||||
s.batchPrevWorlds.emplace_back();
|
|
||||||
}
|
}
|
||||||
// 实例归入自己的 batch(mesh 分组),保证同 batch
|
// 实例归入自己的 batch(mesh 分组),保证同 batch
|
||||||
// 实例在扁平化后连续——firstInstance 语义的前提
|
// 实例在扁平化后连续——firstInstance 语义的前提
|
||||||
const u32 batchIdx = it->second;
|
const u32 bi = batchIdx - 1;
|
||||||
s.result.batches[batchIdx].count++;
|
s.result.batches[bi].count++;
|
||||||
s.batchWorlds[batchIdx].push_back(t.World());
|
s.batchMats[bi].push_back({t.World(), prevWorld});
|
||||||
s.batchPrevWorlds[batchIdx].push_back(prevWorld);
|
|
||||||
s.result.instanceCount++;
|
s.result.instanceCount++;
|
||||||
++totalInstances;
|
++totalInstances;
|
||||||
};
|
};
|
||||||
|
|
||||||
tryAppend(m_MainScratch, mainFrustum, mainLod);
|
tryAppend(m_MainScratch, mainFrustum, mainLod);
|
||||||
for (u32 c = 0; c < kMaxCascades; ++c) {
|
for (u32 c = 0; c < kMaxCascades; ++c) {
|
||||||
|
if (!cascVis[c]) continue; // 粗剔除已判不可见
|
||||||
// 级联降级:越远的级联覆盖范围越大,用越低的 LOD
|
// 级联降级:越远的级联覆盖范围越大,用越低的 LOD
|
||||||
//(阴影本来就是低频信息,误差可接受)
|
//(阴影本来就是低频信息,误差可接受)
|
||||||
tryAppend(m_CascScratch[c], cascFrustum[c],
|
tryAppend(m_CascScratch[c], cascFrustum[c],
|
||||||
@@ -360,13 +378,10 @@ namespace Engine
|
|||||||
b.first = base; // 扁平化后的连续区间基址
|
b.first = base; // 扁平化后的连续区间基址
|
||||||
base += b.count;
|
base += b.count;
|
||||||
dst.batches.push_back(b);
|
dst.batches.push_back(b);
|
||||||
m_Frame.instanceModels.insert(m_Frame.instanceModels.end(),
|
for (const auto& m : s.batchMats[i]) {
|
||||||
s.batchWorlds[i].begin(),
|
m_Frame.instanceModels.push_back(m.cur);
|
||||||
s.batchWorlds[i].end());
|
m_Frame.prevInstanceModels.push_back(m.prev);
|
||||||
m_Frame.prevInstanceModels.insert(
|
}
|
||||||
m_Frame.prevInstanceModels.end(),
|
|
||||||
s.batchPrevWorlds[i].begin(),
|
|
||||||
s.batchPrevWorlds[i].end());
|
|
||||||
}
|
}
|
||||||
dst.instanceCount = s.result.instanceCount;
|
dst.instanceCount = s.result.instanceCount;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user