feat: 模型异步加载——assimp 解析移后台线程(SunTemple 3s 卡顿消除)
Model 拆两段式:ParseFromDisk(static 纯 CPU,assimp 解析 → 几何 + 材质引用,worker 可调)/ FromParsed(GPU 构建:纹理上传 / mesh 缓冲 / 材质描述符,主线程),同步路径等价两段串行。第二个异步任务类型落地, 线程收割骨架抽成 Core/AsyncTask<T>(thread + atomic + Take,覆盖式 Run;pending 接力语义留调用方)。 实测(SunTemple 1056 mesh):解析 2.5s 全程在 worker,主线程持续 渲染;GPU 构建 0.56s。assets 层经 IAssetUploader/AssetManager 重载 传预解析产物(路径缓存去重,路径已缓存则产物丢弃)。 已知边界:纹理解码(backpack 4k PNG ~1s)仍在 GPU 段主线程—— Texture 需支持预解码像素注入,后续演进。
This commit is contained in:
@@ -150,6 +150,8 @@ namespace Engine
|
||||
AssetManager& operator=(const AssetManager&) = delete;
|
||||
|
||||
ModelRef LoadModel(const std::string& path);
|
||||
// 异步路径:解析已在后台线程完成(路径已缓存则产物丢弃去重)
|
||||
ModelRef LoadModel(const std::string& path, ParsedModel&& parsed);
|
||||
Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
|
||||
void UnloadModel(ModelRef h) { m_Models.Unload(h); }
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ namespace Engine::Assets {
|
||||
// 磁盘模型 → 解析 + GPU 上传(失败返回 nullptr)
|
||||
virtual Scope<Model> LoadModel(const std::string& path) = 0;
|
||||
|
||||
// 异步路径:解析已在后台线程完成,此调用只做 GPU 构建(主线程)
|
||||
virtual Scope<Model> LoadModel(const std::string& path,
|
||||
ParsedModel&& parsed) = 0;
|
||||
|
||||
// 程序化纯色平面(size 为半边长)
|
||||
virtual Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/24.
|
||||
//
|
||||
// AsyncTask:单 in-flight 异步任务的通用收割工具(header-only)。
|
||||
//
|
||||
// 异步加载的公共骨架(已验证两次:环境 DecodeHdr / 模型 ParseFromDisk):
|
||||
// worker 线程跑纯 CPU 任务 → 原子标志通知 → 主线程每帧轮询收割。
|
||||
// Vulkan 提交/描述符等 GPU 工作全部留在主线程(设备非线程安全)。
|
||||
//
|
||||
// AsyncTask<ParsedModel> task;
|
||||
// task.Run([] { return Model::ParseFromDisk(path); }); // 发起
|
||||
// if (task.Done()) { auto p = task.Take(); ... } // 每帧收割
|
||||
// // 析构自动 join(worker 只做纯 CPU 工作,join 恒安全)
|
||||
//
|
||||
// 语义:Run 在忙碌时先 join 并丢弃旧结果(覆盖式);"快速连切只保留
|
||||
// 最终意图"的 pending 接力由调用方持有(参照 main.cpp 环境加载)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASYNCTASK_H
|
||||
#define HELLOVULKAN_ASYNCTASK_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
template <typename T>
|
||||
class AsyncTask {
|
||||
public:
|
||||
AsyncTask() = default;
|
||||
~AsyncTask() { Join(); }
|
||||
|
||||
AsyncTask(const AsyncTask&) = delete;
|
||||
AsyncTask& operator=(const AsyncTask&) = delete;
|
||||
|
||||
// 发起任务。忙碌时先等待旧任务完成并放弃其结果。
|
||||
void Run(std::function<T()> job)
|
||||
{
|
||||
Join();
|
||||
m_Busy = true;
|
||||
m_Done.store(false);
|
||||
m_Worker = std::thread([this, j = std::move(job)]() {
|
||||
m_Result = j();
|
||||
m_Done.store(true); // release:结果对主线程 acquire 可见
|
||||
});
|
||||
}
|
||||
|
||||
bool Busy() const { return m_Busy; }
|
||||
bool Done() const { return m_Busy && m_Done.load(); }
|
||||
|
||||
// 收割:join worker 并移交结果。只在 Done() 为 true 时调用。
|
||||
T Take()
|
||||
{
|
||||
m_Worker.join(); // join = 同步点,此后结果归主线程
|
||||
m_Busy = false;
|
||||
T out = std::move(m_Result);
|
||||
m_Result = T{};
|
||||
return out;
|
||||
}
|
||||
|
||||
// 关闭前兜底等待(析构自动调用)
|
||||
void Join()
|
||||
{
|
||||
if (m_Worker.joinable()) {
|
||||
m_Worker.join();
|
||||
}
|
||||
m_Busy = false;
|
||||
}
|
||||
|
||||
private:
|
||||
std::thread m_Worker;
|
||||
std::atomic<bool> m_Done{false};
|
||||
T m_Result{};
|
||||
bool m_Busy = false;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_ASYNCTASK_H
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <Engine/Renderer/Mesh.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -28,14 +29,42 @@ namespace Engine
|
||||
class Texture;
|
||||
class Material;
|
||||
|
||||
// ---- CPU 解析产物(异步加载的跨线程交棒数据)----
|
||||
// 纯数据:几何 + 材质槽位引用(路径 + 因子),无任何 GPU 资源。
|
||||
// worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。
|
||||
struct ParsedMesh {
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<u32> indices;
|
||||
std::array<std::string, Material::kSlotCount> texPaths;
|
||||
MaterialParams params;
|
||||
};
|
||||
struct ParsedModel {
|
||||
std::string path;
|
||||
bool isGltf = false;
|
||||
std::vector<ParsedMesh> meshes;
|
||||
bool Valid() const { return !meshes.empty(); }
|
||||
};
|
||||
|
||||
class Model
|
||||
{
|
||||
public:
|
||||
// 同步便捷路径(等价 ParseFromDisk + FromParsed,启动期小模型可用)
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
const std::string& path);
|
||||
|
||||
// ---- 异步加载两段式(assimp 解析占大模型耗时大头,可搬后台线程)----
|
||||
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
|
||||
static ParsedModel ParseFromDisk(const std::string& path);
|
||||
|
||||
// GPU 构建(必须主线程):纹理上传 / mesh 缓冲 / 材质描述符。
|
||||
// 解析失败返回 nullptr。
|
||||
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
ParsedModel&& parsed);
|
||||
|
||||
// 程序化模型:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
||||
// size 为半边长(平面总边长 = 2*size)。
|
||||
struct ProceduralTag {};
|
||||
@@ -57,9 +86,19 @@ namespace Engine
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
ProceduralTag, f32 halfSize, const std::array<u8, 3>& color);
|
||||
|
||||
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout);
|
||||
|
||||
void LoadModel(const std::string& path);
|
||||
void ProcessNode(aiNode* node, const aiScene* scene);
|
||||
Scope<Mesh> ProcessMesh(aiMesh* mesh, const aiScene* scene);
|
||||
static void ProcessNode(const aiNode* node, const aiScene* scene,
|
||||
std::vector<ParsedMesh>& out,
|
||||
const std::filesystem::path& modelDir,
|
||||
bool isGltf);
|
||||
static ParsedMesh ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
const std::filesystem::path& modelDir,
|
||||
bool isGltf);
|
||||
void InitDefaultTextures();
|
||||
|
||||
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
|
||||
|
||||
@@ -32,6 +32,8 @@ namespace Engine {
|
||||
m_Ibl(ibl) {}
|
||||
|
||||
Scope<Model> LoadModel(const std::string& path) override;
|
||||
Scope<Model> LoadModel(const std::string& path,
|
||||
ParsedModel&& parsed) 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,
|
||||
|
||||
Reference in New Issue
Block a user