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:
2026-09-24 14:32:39 +08:00
parent 641f79d89e
commit 25a365fce7
9 changed files with 202 additions and 35 deletions
+95 -10
View File
@@ -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;
};
}