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:
@@ -27,13 +27,15 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
namespace Assets {
|
||||
|
||||
namespace Engine
|
||||
{
|
||||
namespace Assets
|
||||
{
|
||||
// ---------- 通用资产缓存(不感知 Vulkan / 磁盘细节由 loader 决定) ----------
|
||||
|
||||
template<typename T>
|
||||
class AssetCache {
|
||||
template <typename T>
|
||||
class AssetCache
|
||||
{
|
||||
public:
|
||||
// loader:path → 资产实例(失败返回 nullptr)
|
||||
using LoaderFn = std::function<Scope<T>(const std::string&)>;
|
||||
@@ -41,23 +43,28 @@ namespace Engine {
|
||||
// 加载(带路径去重)。失败返回无效句柄。
|
||||
AssetRef<T> Load(const std::string& path, const LoaderFn& loader)
|
||||
{
|
||||
if (auto it = m_ByPath.find(path); it != m_ByPath.end()) {
|
||||
if (auto it = m_ByPath.find(path); it != m_ByPath.end())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
|
||||
Scope<T> asset = loader(path);
|
||||
if (!asset) {
|
||||
if (!asset)
|
||||
{
|
||||
ENGINE_ERROR("AssetCache: failed to load '{}'", path);
|
||||
return {};
|
||||
}
|
||||
|
||||
AssetRef<T> h;
|
||||
if (!m_Free.empty()) {
|
||||
if (!m_Free.empty())
|
||||
{
|
||||
h.index = m_Free.back();
|
||||
m_Free.pop_back();
|
||||
h.generation = m_Entries[h.index].generation;
|
||||
m_Entries[h.index] = Entry{path, h.generation, std::move(asset), true};
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
h.index = static_cast<u32>(m_Entries.size());
|
||||
h.generation = 1;
|
||||
m_Entries.push_back(Entry{path, h.generation, std::move(asset), true});
|
||||
@@ -70,12 +77,14 @@ namespace Engine {
|
||||
// 句柄 → 资产指针。悬空句柄 FATAL(bug 信号);已卸载返回 nullptr。
|
||||
T* Get(AssetRef<T> h) const
|
||||
{
|
||||
if (!h.Valid() || h.index >= m_Entries.size()) {
|
||||
if (!h.Valid() || h.index >= m_Entries.size())
|
||||
{
|
||||
ENGINE_FATAL("AssetCache::Get: invalid handle (index={}, gen={})",
|
||||
h.index, h.generation);
|
||||
}
|
||||
const Entry& e = m_Entries[h.index];
|
||||
if (e.generation != h.generation) {
|
||||
if (e.generation != h.generation)
|
||||
{
|
||||
ENGINE_FATAL("AssetCache::Get: stale handle for '{}' (gen {} != {})",
|
||||
e.path, h.generation, e.generation);
|
||||
}
|
||||
@@ -114,7 +123,8 @@ namespace Engine {
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
struct Entry
|
||||
{
|
||||
std::string path;
|
||||
u32 generation = 1;
|
||||
Scope<T> asset;
|
||||
@@ -128,7 +138,8 @@ namespace Engine {
|
||||
|
||||
// ---------- AssetManager:全引擎资产入口 ----------
|
||||
|
||||
class AssetManager {
|
||||
class AssetManager
|
||||
{
|
||||
public:
|
||||
// 上传服务由组合根(Application)注入:渲染层实现接口,
|
||||
// 资产层不感知 Vulkan 具体类
|
||||
@@ -148,6 +159,10 @@ namespace Engine {
|
||||
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
|
||||
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
|
||||
EnvRef LoadEnvironment(const std::string& path);
|
||||
// 异步路径:解码已在后台线程完成(若路径已缓存,解码产物被
|
||||
// 丢弃去重——上层可先查缓存再起 worker,此处不做强约束)
|
||||
EnvRef LoadEnvironment(const std::string& path,
|
||||
DecodedEquirect&& decoded);
|
||||
Cubemap* GetEnvironment(EnvRef h) const { return m_Environments.Get(h); }
|
||||
|
||||
AssetCache<Model>& Models() { return m_Models; }
|
||||
@@ -161,7 +176,6 @@ namespace Engine {
|
||||
AssetCache<Model> m_Models;
|
||||
AssetCache<Cubemap> m_Environments;
|
||||
};
|
||||
|
||||
} // namespace Assets
|
||||
} // namespace Engine
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ namespace Engine::Assets {
|
||||
|
||||
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
|
||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
|
||||
|
||||
// 异步加载路径:解码已在后台线程完成,此调用只做 GPU 生成(主线程)
|
||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||
DecodedEquirect&& decoded) = 0;
|
||||
};
|
||||
|
||||
} // namespace Engine::Assets
|
||||
|
||||
@@ -20,11 +20,23 @@
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
|
||||
// 环境的 CPU 源数据(等距柱状 HDR 解码产物,RGBA float 行优先)。
|
||||
// 纯数据无线程状态:异步加载时由后台线程产出(DecodeHdr),
|
||||
// 主线程消费(FromDecoded → GPU 生成)。decode 是切换耗时大头,
|
||||
// 搬出主线程后运行时切换只剩 GPU 段(~40ms)。
|
||||
struct DecodedEquirect {
|
||||
std::vector<f32> pixels;
|
||||
u32 w = 0;
|
||||
u32 h = 0;
|
||||
bool Valid() const { return !pixels.empty() && w > 0 && h > 0; }
|
||||
};
|
||||
|
||||
// GPU 纹理资源三元组(RHI 惯例):image/view/显存分配同生共死,
|
||||
// 析构统一释放——杜绝"建了 image 丢了 view"类半套泄漏。
|
||||
// 上传 helper 直接以此为单位产出,消费端只拿引用计数语义的整单元
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#define HELLOVULKAN_IBLGENERATOR_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Cubemap.h> // DecodedEquirect(解码产物数据契约)
|
||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
@@ -47,9 +48,18 @@ namespace Engine {
|
||||
IblGenerator(const IblGenerator&) = delete;
|
||||
IblGenerator& operator=(const IblGenerator&) = delete;
|
||||
|
||||
// ---- 同步便捷路径(等价 DecodeHdr + FromDecoded)----
|
||||
// 等距柱状 HDR → 环境四件套(失败返回 nullptr,上层走 Neutral 兜底)
|
||||
Scope<Cubemap> FromHdrEquirect(const std::string& path, u32 faceSize);
|
||||
|
||||
// ---- 异步加载两段式(decode 是切换耗时大头,可搬后台线程)----
|
||||
// 纯 CPU:HDR 文件 → 像素数据。无线程状态,worker 线程可安全调用。
|
||||
static DecodedEquirect DecodeHdr(const std::string& path);
|
||||
|
||||
// GPU 生成(必须主线程):预解码数据 → 四件套。debugName 仅用于日志。
|
||||
Scope<Cubemap> FromDecoded(const DecodedEquirect& decoded, u32 faceSize,
|
||||
const std::string& debugName);
|
||||
|
||||
// 中性兜底:常数色环境 + 真实 BRDF LUT(描述符恒有效)
|
||||
Scope<Cubemap> Neutral(const glm::vec3& rgb);
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace Engine {
|
||||
Scope<Model> LoadModel(const std::string& path) override;
|
||||
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
|
||||
Scope<Cubemap> LoadEnvironment(const std::string& path) override;
|
||||
Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||
DecodedEquirect&& decoded) override;
|
||||
|
||||
private:
|
||||
Device& m_Device;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user