feat: 环境异步加载——HDR 解码移后台线程,运行时切换不卡主线程
decode 是切换耗时大头(4k ≈ 130-160ms),搬出主线程后运行时切换 只剩 GPU 段(~40ms)。IblGenerator 拆两段式:DecodeHdr(static, 纯 CPU 无线程状态,worker 可调)+ FromDecoded(GPU 生成,主线程); FromHdrEquirect 保留为同步便捷路径。Assets 层经 IAssetUploader 重载 + AssetManager 重载传递预解码产物(路径已缓存则丢弃去重)。 App 层单 in-flight 状态机:worker 只产出纯 CPU 像素数据,忙肋期 新请求记 pending 接力(只保留最终意图),OnUpdate 每帧收割 + SetEnvironment 换绑,析构 join。 实测(RTX 5060 Laptop,4 实例网格 + 运行中连切 3 环境): ~180fps 无感知尖峰;0 validation。
This commit is contained in:
+95
-10
@@ -16,6 +16,7 @@
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Renderer/Renderer.h>
|
||||
#include <Engine/Renderer/IblGenerator.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
@@ -24,6 +25,9 @@
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
|
||||
using namespace Engine;
|
||||
using namespace Engine::ECS;
|
||||
@@ -52,6 +56,15 @@ namespace
|
||||
{
|
||||
}
|
||||
|
||||
~SandboxApp() override
|
||||
{
|
||||
// worker 只做 CPU 解码(不碰 Vulkan),在任何 GPU 资源
|
||||
// 析构之前 join 即安全;派生析构先于基类成员析构执行
|
||||
if (m_EnvWorker.joinable()) {
|
||||
m_EnvWorker.join();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// ---- 生命周期 ----
|
||||
|
||||
@@ -75,9 +88,17 @@ namespace
|
||||
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
|
||||
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
|
||||
|
||||
m_Spinners.push_back(scene.CreateEntity("backpack"));
|
||||
m_Spinners[0].AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
|
||||
m_Spinners[0].GetComponent<TransformComponent>().SetScale(glm::vec3{1.0});
|
||||
int countSize = 2;
|
||||
for (int i = 0; i < countSize; i ++)
|
||||
{
|
||||
for (int j = 0; j < countSize; j ++)
|
||||
{
|
||||
auto m = scene.CreateEntity();
|
||||
m.AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
|
||||
m.GetComponent<TransformComponent>().SetPosition(glm::vec3{(i - countSize * 0.5) * 2, 0, (j - countSize * 0.5) * 2});
|
||||
m_Spinners.push_back(m);
|
||||
}
|
||||
}
|
||||
|
||||
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
|
||||
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
|
||||
@@ -134,6 +155,9 @@ namespace
|
||||
|
||||
void OnUpdate(f32 deltaTime) override
|
||||
{
|
||||
// 异步环境加载:每帧收割完成的解码结果
|
||||
PollEnvironment();
|
||||
|
||||
// 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff
|
||||
// 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到)
|
||||
if (!m_PauseAnim)
|
||||
@@ -157,6 +181,61 @@ namespace
|
||||
|
||||
// ---- 事件 ----
|
||||
|
||||
// ---- 异步环境加载(decode 后台线程 / GPU 生成主线程)----
|
||||
// decode 占切换耗时大头(4k ≈ 130ms);worker 只产出纯 CPU
|
||||
// 像素数据,Vulkan 生成回主线程(设备非线程安全)。
|
||||
// 同一时刻最多一个 in-flight,忙碌期的新请求记为 pending 接力。
|
||||
void RequestEnvironment(int idx)
|
||||
{
|
||||
if (m_EnvLoadBusy)
|
||||
{
|
||||
m_EnvPending = idx;
|
||||
return;
|
||||
}
|
||||
StartEnvDecode(idx);
|
||||
}
|
||||
|
||||
void StartEnvDecode(int idx)
|
||||
{
|
||||
m_EnvLoadBusy = true;
|
||||
m_EnvLoadDone.store(false);
|
||||
m_EnvLoadingPath = kEnvPaths[idx];
|
||||
m_EnvWorker = std::thread([this, idx]() {
|
||||
m_EnvDecoded = IblGenerator::DecodeHdr(kEnvPaths[idx]);
|
||||
m_EnvLoadDone.store(true); // release:解码结果对主线程可见
|
||||
});
|
||||
}
|
||||
|
||||
void PollEnvironment()
|
||||
{
|
||||
if (!m_EnvLoadBusy || !m_EnvLoadDone.load())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_EnvWorker.join(); // join = 同步点,解码结果此后归主线程
|
||||
m_EnvLoadBusy = false;
|
||||
|
||||
if (m_EnvDecoded.Valid())
|
||||
{
|
||||
const auto ref = GetAssets().LoadEnvironment(
|
||||
m_EnvLoadingPath, std::move(m_EnvDecoded));
|
||||
if (ref.Valid())
|
||||
{
|
||||
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(ref));
|
||||
}
|
||||
}
|
||||
m_EnvDecoded = {};
|
||||
|
||||
// 忙碌期间的最新请求接力(rapid switch 只保留最终意图)
|
||||
if (m_EnvPending >= 0)
|
||||
{
|
||||
const int next = m_EnvPending;
|
||||
m_EnvPending = -1;
|
||||
StartEnvDecode(next);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 叠加层 UI(唯一允许出现 ImGui 控件的地方)----
|
||||
|
||||
void OnRenderOverlay() override
|
||||
@@ -240,14 +319,12 @@ namespace
|
||||
int envIdx = m_EnvIndex;
|
||||
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
|
||||
{
|
||||
const auto ref = GetAssets().LoadEnvironment(kEnvPaths[envIdx]);
|
||||
if (ref.Valid())
|
||||
{
|
||||
renderer.SetEnvironment(GetAssets().GetEnvironment(ref));
|
||||
m_EnvIndex = envIdx;
|
||||
}
|
||||
// 异步:解码后台线程,完成后 OnUpdate 里换绑
|
||||
m_EnvIndex = envIdx;
|
||||
RequestEnvironment(envIdx);
|
||||
}
|
||||
ImGui::Text("(switch = compute pre-integrate, ms-level)");
|
||||
ImGui::Text(m_EnvLoadBusy ? "(decoding...)" :
|
||||
"(switch = compute pre-integrate, ms-level)");
|
||||
|
||||
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
|
||||
const auto& viewNames = renderer.GetDebugViewNames();
|
||||
@@ -376,6 +453,14 @@ namespace
|
||||
f32 m_Time = 0.0f;
|
||||
bool m_PauseAnim = true; // 冻结动画时钟(帧历史 diff 验证用)
|
||||
int m_EnvIndex = kDefaultEnv;
|
||||
|
||||
// ---- 异步环境加载状态(单 in-flight + pending 接力)----
|
||||
std::thread m_EnvWorker;
|
||||
std::string m_EnvLoadingPath;
|
||||
DecodedEquirect m_EnvDecoded; // worker 写 → join 后主线程读
|
||||
std::atomic<bool> m_EnvLoadDone{false};
|
||||
bool m_EnvLoadBusy = false;
|
||||
int m_EnvPending = -1;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user