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/ECS/Components.h>
|
||||||
#include <Engine/Assets/AssetManager.h>
|
#include <Engine/Assets/AssetManager.h>
|
||||||
#include <Engine/Renderer/Renderer.h>
|
#include <Engine/Renderer/Renderer.h>
|
||||||
|
#include <Engine/Renderer/IblGenerator.h>
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
@@ -24,6 +25,9 @@
|
|||||||
|
|
||||||
#include <GLFW/glfw3.h>
|
#include <GLFW/glfw3.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
|
||||||
using namespace Engine;
|
using namespace Engine;
|
||||||
using namespace Engine::ECS;
|
using namespace Engine::ECS;
|
||||||
@@ -52,6 +56,15 @@ namespace
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~SandboxApp() override
|
||||||
|
{
|
||||||
|
// worker 只做 CPU 解码(不碰 Vulkan),在任何 GPU 资源
|
||||||
|
// 析构之前 join 即安全;派生析构先于基类成员析构执行
|
||||||
|
if (m_EnvWorker.joinable()) {
|
||||||
|
m_EnvWorker.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// ---- 生命周期 ----
|
// ---- 生命周期 ----
|
||||||
|
|
||||||
@@ -75,9 +88,17 @@ namespace
|
|||||||
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
|
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");
|
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
|
||||||
|
|
||||||
m_Spinners.push_back(scene.CreateEntity("backpack"));
|
int countSize = 2;
|
||||||
m_Spinners[0].AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
|
for (int i = 0; i < countSize; i ++)
|
||||||
m_Spinners[0].GetComponent<TransformComponent>().SetScale(glm::vec3{1.0});
|
{
|
||||||
|
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)
|
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
|
||||||
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
|
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
|
||||||
@@ -134,6 +155,9 @@ namespace
|
|||||||
|
|
||||||
void OnUpdate(f32 deltaTime) override
|
void OnUpdate(f32 deltaTime) override
|
||||||
{
|
{
|
||||||
|
// 异步环境加载:每帧收割完成的解码结果
|
||||||
|
PollEnvironment();
|
||||||
|
|
||||||
// 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff
|
// 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff
|
||||||
// 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到)
|
// 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到)
|
||||||
if (!m_PauseAnim)
|
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 控件的地方)----
|
// ---- 叠加层 UI(唯一允许出现 ImGui 控件的地方)----
|
||||||
|
|
||||||
void OnRenderOverlay() override
|
void OnRenderOverlay() override
|
||||||
@@ -240,14 +319,12 @@ namespace
|
|||||||
int envIdx = m_EnvIndex;
|
int envIdx = m_EnvIndex;
|
||||||
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
|
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
|
||||||
{
|
{
|
||||||
const auto ref = GetAssets().LoadEnvironment(kEnvPaths[envIdx]);
|
// 异步:解码后台线程,完成后 OnUpdate 里换绑
|
||||||
if (ref.Valid())
|
m_EnvIndex = envIdx;
|
||||||
{
|
RequestEnvironment(envIdx);
|
||||||
renderer.SetEnvironment(GetAssets().GetEnvironment(ref));
|
|
||||||
m_EnvIndex = envIdx;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ImGui::Text("(switch = compute pre-integrate, ms-level)");
|
ImGui::Text(m_EnvLoadBusy ? "(decoding...)" :
|
||||||
|
"(switch = compute pre-integrate, ms-level)");
|
||||||
|
|
||||||
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
|
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
|
||||||
const auto& viewNames = renderer.GetDebugViewNames();
|
const auto& viewNames = renderer.GetDebugViewNames();
|
||||||
@@ -376,6 +453,14 @@ namespace
|
|||||||
f32 m_Time = 0.0f;
|
f32 m_Time = 0.0f;
|
||||||
bool m_PauseAnim = true; // 冻结动画时钟(帧历史 diff 验证用)
|
bool m_PauseAnim = true; // 冻结动画时钟(帧历史 diff 验证用)
|
||||||
int m_EnvIndex = kDefaultEnv;
|
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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,15 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine
|
||||||
namespace Assets {
|
{
|
||||||
|
namespace Assets
|
||||||
|
{
|
||||||
// ---------- 通用资产缓存(不感知 Vulkan / 磁盘细节由 loader 决定) ----------
|
// ---------- 通用资产缓存(不感知 Vulkan / 磁盘细节由 loader 决定) ----------
|
||||||
|
|
||||||
template<typename T>
|
template <typename T>
|
||||||
class AssetCache {
|
class AssetCache
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
// loader:path → 资产实例(失败返回 nullptr)
|
// loader:path → 资产实例(失败返回 nullptr)
|
||||||
using LoaderFn = std::function<Scope<T>(const std::string&)>;
|
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)
|
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;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
Scope<T> asset = loader(path);
|
Scope<T> asset = loader(path);
|
||||||
if (!asset) {
|
if (!asset)
|
||||||
|
{
|
||||||
ENGINE_ERROR("AssetCache: failed to load '{}'", path);
|
ENGINE_ERROR("AssetCache: failed to load '{}'", path);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
AssetRef<T> h;
|
AssetRef<T> h;
|
||||||
if (!m_Free.empty()) {
|
if (!m_Free.empty())
|
||||||
|
{
|
||||||
h.index = m_Free.back();
|
h.index = m_Free.back();
|
||||||
m_Free.pop_back();
|
m_Free.pop_back();
|
||||||
h.generation = m_Entries[h.index].generation;
|
h.generation = m_Entries[h.index].generation;
|
||||||
m_Entries[h.index] = Entry{path, h.generation, std::move(asset), true};
|
m_Entries[h.index] = Entry{path, h.generation, std::move(asset), true};
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
h.index = static_cast<u32>(m_Entries.size());
|
h.index = static_cast<u32>(m_Entries.size());
|
||||||
h.generation = 1;
|
h.generation = 1;
|
||||||
m_Entries.push_back(Entry{path, h.generation, std::move(asset), true});
|
m_Entries.push_back(Entry{path, h.generation, std::move(asset), true});
|
||||||
@@ -70,12 +77,14 @@ namespace Engine {
|
|||||||
// 句柄 → 资产指针。悬空句柄 FATAL(bug 信号);已卸载返回 nullptr。
|
// 句柄 → 资产指针。悬空句柄 FATAL(bug 信号);已卸载返回 nullptr。
|
||||||
T* Get(AssetRef<T> h) const
|
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={})",
|
ENGINE_FATAL("AssetCache::Get: invalid handle (index={}, gen={})",
|
||||||
h.index, h.generation);
|
h.index, h.generation);
|
||||||
}
|
}
|
||||||
const Entry& e = m_Entries[h.index];
|
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 {} != {})",
|
ENGINE_FATAL("AssetCache::Get: stale handle for '{}' (gen {} != {})",
|
||||||
e.path, h.generation, e.generation);
|
e.path, h.generation, e.generation);
|
||||||
}
|
}
|
||||||
@@ -114,7 +123,8 @@ namespace Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Entry {
|
struct Entry
|
||||||
|
{
|
||||||
std::string path;
|
std::string path;
|
||||||
u32 generation = 1;
|
u32 generation = 1;
|
||||||
Scope<T> asset;
|
Scope<T> asset;
|
||||||
@@ -128,7 +138,8 @@ namespace Engine {
|
|||||||
|
|
||||||
// ---------- AssetManager:全引擎资产入口 ----------
|
// ---------- AssetManager:全引擎资产入口 ----------
|
||||||
|
|
||||||
class AssetManager {
|
class AssetManager
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
// 上传服务由组合根(Application)注入:渲染层实现接口,
|
// 上传服务由组合根(Application)注入:渲染层实现接口,
|
||||||
// 资产层不感知 Vulkan 具体类
|
// 资产层不感知 Vulkan 具体类
|
||||||
@@ -148,6 +159,10 @@ namespace Engine {
|
|||||||
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
|
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
|
||||||
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
|
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
|
||||||
EnvRef LoadEnvironment(const std::string& path);
|
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); }
|
Cubemap* GetEnvironment(EnvRef h) const { return m_Environments.Get(h); }
|
||||||
|
|
||||||
AssetCache<Model>& Models() { return m_Models; }
|
AssetCache<Model>& Models() { return m_Models; }
|
||||||
@@ -161,7 +176,6 @@ namespace Engine {
|
|||||||
AssetCache<Model> m_Models;
|
AssetCache<Model> m_Models;
|
||||||
AssetCache<Cubemap> m_Environments;
|
AssetCache<Cubemap> m_Environments;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Assets
|
} // namespace Assets
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ namespace Engine::Assets {
|
|||||||
|
|
||||||
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
|
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
|
||||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
|
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
|
||||||
|
|
||||||
|
// 异步加载路径:解码已在后台线程完成,此调用只做 GPU 生成(主线程)
|
||||||
|
virtual Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||||
|
DecodedEquirect&& decoded) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine::Assets
|
} // namespace Engine::Assets
|
||||||
|
|||||||
@@ -20,11 +20,23 @@
|
|||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
class Device;
|
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/显存分配同生共死,
|
// GPU 纹理资源三元组(RHI 惯例):image/view/显存分配同生共死,
|
||||||
// 析构统一释放——杜绝"建了 image 丢了 view"类半套泄漏。
|
// 析构统一释放——杜绝"建了 image 丢了 view"类半套泄漏。
|
||||||
// 上传 helper 直接以此为单位产出,消费端只拿引用计数语义的整单元
|
// 上传 helper 直接以此为单位产出,消费端只拿引用计数语义的整单元
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#define HELLOVULKAN_IBLGENERATOR_H
|
#define HELLOVULKAN_IBLGENERATOR_H
|
||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
|
#include <Engine/Renderer/Cubemap.h> // DecodedEquirect(解码产物数据契约)
|
||||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
@@ -47,9 +48,18 @@ namespace Engine {
|
|||||||
IblGenerator(const IblGenerator&) = delete;
|
IblGenerator(const IblGenerator&) = delete;
|
||||||
IblGenerator& operator=(const IblGenerator&) = delete;
|
IblGenerator& operator=(const IblGenerator&) = delete;
|
||||||
|
|
||||||
|
// ---- 同步便捷路径(等价 DecodeHdr + FromDecoded)----
|
||||||
// 等距柱状 HDR → 环境四件套(失败返回 nullptr,上层走 Neutral 兜底)
|
// 等距柱状 HDR → 环境四件套(失败返回 nullptr,上层走 Neutral 兜底)
|
||||||
Scope<Cubemap> FromHdrEquirect(const std::string& path, u32 faceSize);
|
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(描述符恒有效)
|
// 中性兜底:常数色环境 + 真实 BRDF LUT(描述符恒有效)
|
||||||
Scope<Cubemap> Neutral(const glm::vec3& rgb);
|
Scope<Cubemap> Neutral(const glm::vec3& rgb);
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ namespace Engine {
|
|||||||
Scope<Model> LoadModel(const std::string& path) override;
|
Scope<Model> LoadModel(const std::string& path) override;
|
||||||
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) 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) override;
|
||||||
|
Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||||
|
DecodedEquirect&& decoded) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Device& m_Device;
|
Device& m_Device;
|
||||||
|
|||||||
@@ -37,4 +37,15 @@ namespace Engine::Assets {
|
|||||||
[this](const std::string& p) { return m_Uploader.LoadEnvironment(p); });
|
[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
|
} // namespace Engine::Assets
|
||||||
|
|||||||
@@ -197,26 +197,48 @@ namespace Engine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scope<Cubemap> IblGenerator::FromHdrEquirect(const std::string& path,
|
DecodedEquirect IblGenerator::DecodeHdr(const std::string& path)
|
||||||
u32 faceSize)
|
|
||||||
{
|
{
|
||||||
const auto t0 = std::chrono::steady_clock::now();
|
// stbi_loadf 无共享可变状态(失败原因每次调用独立),可并发调用
|
||||||
|
|
||||||
int w = 0, h = 0, channels = 0;
|
int w = 0, h = 0, channels = 0;
|
||||||
f32* pixels = stbi_loadf(path.c_str(), &w, &h, &channels, STBI_rgb_alpha);
|
f32* pixels = stbi_loadf(path.c_str(), &w, &h, &channels, STBI_rgb_alpha);
|
||||||
if (!pixels) {
|
if (!pixels) {
|
||||||
ENGINE_ERROR("IblGenerator: failed to load HDR: {}", path);
|
ENGINE_ERROR("IblGenerator: failed to load HDR: {}", path);
|
||||||
return nullptr;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const f32 decodeMs = MsSince(t0);
|
DecodedEquirect decoded;
|
||||||
auto cubemap = Generate(pixels, static_cast<u32>(w),
|
decoded.w = static_cast<u32>(w);
|
||||||
static_cast<u32>(h), faceSize, path);
|
decoded.h = static_cast<u32>(h);
|
||||||
|
decoded.pixels.assign(pixels,
|
||||||
|
pixels + static_cast<size_t>(w) * h * 4);
|
||||||
stbi_image_free(pixels);
|
stbi_image_free(pixels);
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
ENGINE_INFO("IblGenerator: HDR decode {:.2f} ms",
|
Scope<Cubemap> IblGenerator::FromDecoded(const DecodedEquirect& decoded,
|
||||||
decodeMs);
|
u32 faceSize,
|
||||||
return cubemap;
|
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)
|
Scope<Cubemap> IblGenerator::Neutral(const glm::vec3& rgb)
|
||||||
|
|||||||
@@ -27,4 +27,11 @@ namespace Engine {
|
|||||||
return m_Ibl.FromHdrEquirect(path, 1024);
|
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
|
} // namespace Engine
|
||||||
|
|||||||
Reference in New Issue
Block a user