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
+11
View File
@@ -37,4 +37,15 @@ namespace Engine::Assets {
[this](const std::string& p) { return m_Uploader.LoadEnvironment(p); });
}
EnvRef AssetManager::LoadEnvironment(const std::string& path,
DecodedEquirect&& decoded)
{
// 预解码数据移交 loader(Load 同步调用 lambda,引用捕获安全);
// 路径已缓存则 loader 不执行,decoded 自然丢弃(去重)
return m_Environments.Load(path,
[this, &decoded](const std::string& p) {
return m_Uploader.LoadEnvironment(p, std::move(decoded));
});
}
} // namespace Engine::Assets
+33 -11
View File
@@ -197,26 +197,48 @@ namespace Engine
}
}
Scope<Cubemap> IblGenerator::FromHdrEquirect(const std::string& path,
u32 faceSize)
DecodedEquirect IblGenerator::DecodeHdr(const std::string& path)
{
const auto t0 = std::chrono::steady_clock::now();
// stbi_loadf 无共享可变状态(失败原因每次调用独立),可并发调用
int w = 0, h = 0, channels = 0;
f32* pixels = stbi_loadf(path.c_str(), &w, &h, &channels, STBI_rgb_alpha);
if (!pixels) {
ENGINE_ERROR("IblGenerator: failed to load HDR: {}", path);
return nullptr;
return {};
}
const f32 decodeMs = MsSince(t0);
auto cubemap = Generate(pixels, static_cast<u32>(w),
static_cast<u32>(h), faceSize, path);
DecodedEquirect decoded;
decoded.w = static_cast<u32>(w);
decoded.h = static_cast<u32>(h);
decoded.pixels.assign(pixels,
pixels + static_cast<size_t>(w) * h * 4);
stbi_image_free(pixels);
return decoded;
}
ENGINE_INFO("IblGenerator: HDR decode {:.2f} ms",
decodeMs);
return cubemap;
Scope<Cubemap> IblGenerator::FromDecoded(const DecodedEquirect& decoded,
u32 faceSize,
const std::string& debugName)
{
if (!decoded.Valid()) {
ENGINE_ERROR("IblGenerator: invalid decoded equirect ({})",
debugName);
return nullptr;
}
return Generate(decoded.pixels.data(), decoded.w, decoded.h,
faceSize, debugName);
}
Scope<Cubemap> IblGenerator::FromHdrEquirect(const std::string& path,
u32 faceSize)
{
const auto t0 = std::chrono::steady_clock::now();
DecodedEquirect decoded = DecodeHdr(path);
if (!decoded.Valid()) {
return nullptr;
}
ENGINE_INFO("IblGenerator: HDR decode {:.2f} ms", MsSince(t0));
return FromDecoded(decoded, faceSize, path);
}
Scope<Cubemap> IblGenerator::Neutral(const glm::vec3& rgb)
@@ -27,4 +27,11 @@ namespace Engine {
return m_Ibl.FromHdrEquirect(path, 1024);
}
Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(
const std::string& path, DecodedEquirect&& decoded)
{
// 异步路径:decode 已在 worker 线程完成,此处只剩 GPU 段
return m_Ibl.FromDecoded(decoded, 1024, path);
}
} // namespace Engine