ADD: async glTF loading

This commit is contained in:
2025-12-04 00:04:56 +09:00
parent 01b28174be
commit 73e15ee456
16 changed files with 755 additions and 51 deletions

View File

@@ -0,0 +1,298 @@
#include "async_loader.h"
#include <utility>
#include "manager.h"
#include "core/engine.h"
#include "scene/vk_scene.h"
AsyncAssetLoader::AsyncAssetLoader() = default;
AsyncAssetLoader::~AsyncAssetLoader()
{
shutdown();
}
void AsyncAssetLoader::init(VulkanEngine *engine, AssetManager *assets, TextureCache *textures, uint32_t worker_count)
{
_engine = engine;
_assets = assets;
_textures = textures;
if (worker_count == 0)
{
worker_count = 1;
}
start_workers(worker_count);
}
void AsyncAssetLoader::shutdown()
{
stop_workers();
std::lock_guard<std::mutex> lock(_jobs_mutex);
_jobs.clear();
_queue.clear();
}
void AsyncAssetLoader::start_workers(uint32_t count)
{
if (_running.load(std::memory_order_acquire))
{
return;
}
_running.store(true, std::memory_order_release);
_workers.reserve(count);
for (uint32_t i = 0; i < count; ++i)
{
_workers.emplace_back([this]() { worker_loop(); });
}
}
void AsyncAssetLoader::stop_workers()
{
if (!_running.exchange(false, std::memory_order_acq_rel))
{
return;
}
_jobs_cv.notify_all();
for (auto &t : _workers)
{
if (t.joinable())
{
t.join();
}
}
_workers.clear();
}
AsyncAssetLoader::JobID AsyncAssetLoader::load_gltf_async(const std::string &scene_name,
const std::string &model_relative_path,
const glm::mat4 &transform)
{
if (!_assets)
{
return 0;
}
JobID id = _next_id.fetch_add(1, std::memory_order_relaxed);
std::unique_ptr<Job> job = std::make_unique<Job>(/*args...*/);
job->id = id;
job->scene_name = scene_name;
job->model_relative_path = model_relative_path;
job->transform = transform;
job->progress.store(0.0f, std::memory_order_relaxed);
job->state.store(JobState::Pending, std::memory_order_relaxed);
// Prefetch textures on the main thread and remember handles for progress.
if (_textures)
{
AssetManager::GLTFTexturePrefetchResult pref = _assets->prefetchGLTFTexturesWithHandles(model_relative_path);
job->texture_handles = std::move(pref.handles);
}
{
std::lock_guard<std::mutex> lock(_jobs_mutex);
_jobs.emplace(id, std::move(job));
_queue.push_back(id);
}
_jobs_cv.notify_one();
return id;
}
bool AsyncAssetLoader::get_job_status(JobID id, JobState &out_state, float &out_progress, std::string *out_error)
{
std::lock_guard<std::mutex> lock(_jobs_mutex);
auto it = _jobs.find(id);
if (it == _jobs.end())
{
return false;
}
Job &job = *it->second;
JobState state = job.state.load(std::memory_order_acquire);
float gltf_progress = job.progress.load(std::memory_order_relaxed);
float tex_fraction = 0.0f;
if (_textures && !job.texture_handles.empty())
{
size_t total = job.texture_handles.size();
size_t resident = 0;
for (TextureCache::TextureHandle h : job.texture_handles)
{
if (h == TextureCache::InvalidHandle)
{
continue;
}
auto st = _textures->state(h);
if (st == TextureCache::EntryState::Resident)
{
resident++;
}
}
if (total > 0)
{
tex_fraction = static_cast<float>(resident) / static_cast<float>(total);
}
}
float combined = gltf_progress;
if (tex_fraction > 0.0f)
{
combined = 0.7f * gltf_progress + 0.3f * tex_fraction;
}
if (state == JobState::Completed || state == JobState::Failed)
{
combined = 1.0f;
}
out_state = state;
out_progress = combined;
if (out_error)
{
*out_error = job.error;
}
return true;
}
void AsyncAssetLoader::debug_snapshot(std::vector<DebugJob> &out_jobs)
{
out_jobs.clear();
std::lock_guard<std::mutex> lock(_jobs_mutex);
out_jobs.reserve(_jobs.size());
for (auto &[id, jobPtr] : _jobs)
{
const Job &job = *jobPtr;
float gltf_progress = job.progress.load(std::memory_order_relaxed);
JobState state = job.state.load(std::memory_order_acquire);
float tex_fraction = 0.0f;
size_t tex_total = job.texture_handles.size();
size_t tex_resident = 0;
if (_textures && tex_total > 0)
{
for (TextureCache::TextureHandle h : job.texture_handles)
{
if (h == TextureCache::InvalidHandle)
{
continue;
}
if (_textures->state(h) == TextureCache::EntryState::Resident)
{
tex_resident++;
}
}
if (tex_total > 0)
{
tex_fraction = static_cast<float>(tex_resident) / static_cast<float>(tex_total);
}
}
float combined = gltf_progress;
if (tex_fraction > 0.0f)
{
combined = 0.7f * gltf_progress + 0.3f * tex_fraction;
}
if (state == JobState::Completed || state == JobState::Failed)
{
combined = 1.0f;
}
DebugJob dbg{};
dbg.id = job.id;
dbg.state = state;
dbg.progress = combined;
dbg.scene_name = job.scene_name;
dbg.model_relative_path = job.model_relative_path;
dbg.texture_count = tex_total;
dbg.textures_resident = tex_resident;
out_jobs.push_back(std::move(dbg));
}
}
void AsyncAssetLoader::pump_main_thread(SceneManager &scene)
{
std::lock_guard<std::mutex> lock(_jobs_mutex);
for (auto &[id, job] : _jobs)
{
JobState state = job->state.load(std::memory_order_acquire);
if (state == JobState::Completed && !job->committed_to_scene)
{
if (job->scene)
{
if (job->scene->debugName.empty())
{
job->scene->debugName = job->model_relative_path;
}
scene.addGLTFInstance(job->scene_name, job->scene, job->transform);
}
job->committed_to_scene = true;
}
}
}
void AsyncAssetLoader::worker_loop()
{
while (true)
{
JobID id = 0;
{
std::unique_lock<std::mutex> lock(_jobs_mutex);
_jobs_cv.wait(lock, [this]() { return !_queue.empty() || !_running.load(std::memory_order_acquire); });
if (!_running.load(std::memory_order_acquire) && _queue.empty())
{
return;
}
if (_queue.empty())
{
continue;
}
id = _queue.front();
_queue.pop_front();
}
Job *job = nullptr;
{
std::lock_guard<std::mutex> lock(_jobs_mutex);
auto it = _jobs.find(id);
if (it == _jobs.end())
{
continue;
}
job = it->second.get();
}
job->state.store(JobState::Running, std::memory_order_release);
job->progress.store(0.01f, std::memory_order_relaxed);
GLTFLoadCallbacks cb{};
cb.on_progress = [job](float v)
{
job->progress.store(v, std::memory_order_relaxed);
};
cb.is_cancelled = [job]() -> bool
{
return job->state.load(std::memory_order_acquire) == JobState::Cancelled;
};
std::optional<std::shared_ptr<LoadedGLTF> > loaded = _assets->loadGLTF(job->model_relative_path, &cb);
if (!loaded.has_value() || !loaded.value())
{
job->error = "loadGLTF failed or returned empty scene";
job->state.store(JobState::Failed, std::memory_order_release);
job->progress.store(1.0f, std::memory_order_relaxed);
continue;
}
job->scene = loaded.value();
job->progress.store(1.0f, std::memory_order_relaxed);
job->state.store(JobState::Completed, std::memory_order_release);
}
}

View File

@@ -0,0 +1,97 @@
#pragma once
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include <thread>
#include <glm/mat4x4.hpp>
#include "scene/vk_loader.h"
#include "core/assets/texture_cache.h"
class VulkanEngine;
class AssetManager;
class SceneManager;
// Small orchestrator for asynchronous glTF asset jobs.
// - CPU work (file I/O, fastgltf parsing, mesh/BVH build) runs on worker threads.
// - GPU uploads are still deferred through ResourceManager and the Render Graph.
// - Texture streaming and residency are tracked via TextureCache for progress.
class AsyncAssetLoader
{
public:
using JobID = uint32_t;
enum class JobState { Pending, Running, Completed, Failed, Cancelled };
AsyncAssetLoader();
~AsyncAssetLoader();
void init(VulkanEngine *engine, AssetManager *assets, TextureCache *textures, uint32_t worker_count = 1);
void shutdown();
JobID load_gltf_async(const std::string &scene_name,
const std::string &model_relative_path,
const glm::mat4 &transform);
bool get_job_status(JobID id, JobState &out_state, float &out_progress, std::string *out_error = nullptr);
// Main-thread integration: commit completed jobs into the SceneManager.
void pump_main_thread(SceneManager &scene);
struct DebugJob
{
JobID id{0};
JobState state{JobState::Pending};
float progress{0.0f};
std::string scene_name;
std::string model_relative_path;
size_t texture_count{0};
size_t textures_resident{0};
};
// Debug-only snapshot of current jobs for UI/tools (main-thread only).
void debug_snapshot(std::vector<DebugJob> &out_jobs);
private:
struct Job
{
JobID id{0};
std::string scene_name;
std::string model_relative_path;
glm::mat4 transform{1.0f};
std::shared_ptr<LoadedGLTF> scene;
std::atomic<float> progress{0.0f};
std::atomic<JobState> state{JobState::Pending};
std::string error;
bool committed_to_scene{false};
// Texture handles associated with this glTF (prefetched via TextureCache).
std::vector<TextureCache::TextureHandle> texture_handles;
};
void start_workers(uint32_t count);
void stop_workers();
void worker_loop();
VulkanEngine *_engine{nullptr};
AssetManager *_assets{nullptr};
TextureCache *_textures{nullptr};
std::atomic<bool> _running{false};
std::vector<std::thread> _workers;
std::mutex _jobs_mutex;
std::condition_variable _jobs_cv;
std::unordered_map<JobID, std::unique_ptr<Job>> _jobs;
std::deque<JobID> _queue;
std::atomic<JobID> _next_id{1};
};

View File

@@ -52,7 +52,10 @@ void AssetManager::cleanup()
_meshCache.clear();
_meshMaterialBuffers.clear();
_meshOwnedImages.clear();
_gltfCacheByPath.clear();
{
std::lock_guard<std::mutex> lock(_gltfMutex);
_gltfCacheByPath.clear();
}
}
std::string AssetManager::shaderPath(std::string_view name) const
@@ -71,6 +74,12 @@ std::string AssetManager::modelPath(std::string_view name) const
}
std::optional<std::shared_ptr<LoadedGLTF> > AssetManager::loadGLTF(std::string_view nameOrPath)
{
return loadGLTF(nameOrPath, nullptr);
}
std::optional<std::shared_ptr<LoadedGLTF> > AssetManager::loadGLTF(std::string_view nameOrPath,
const GLTFLoadCallbacks *cb)
{
if (!_engine) return {};
if (nameOrPath.empty()) return {};
@@ -82,18 +91,22 @@ std::optional<std::shared_ptr<LoadedGLTF> > AssetManager::loadGLTF(std::string_v
keyPath = std::filesystem::weakly_canonical(keyPath, ec);
std::string key = (ec ? resolved : keyPath.string());
if (auto it = _gltfCacheByPath.find(key); it != _gltfCacheByPath.end())
{
if (auto sp = it->second.lock())
std::lock_guard<std::mutex> lock(_gltfMutex);
if (auto it = _gltfCacheByPath.find(key); it != _gltfCacheByPath.end())
{
fmt::println("[AssetManager] loadGLTF cache hit key='{}' path='{}' ptr={}", key, resolved,
static_cast<const void *>(sp.get()));
return sp;
if (auto sp = it->second.lock())
{
fmt::println("[AssetManager] loadGLTF cache hit key='{}' path='{}' ptr={}", key, resolved,
static_cast<const void *>(sp.get()));
return sp;
}
fmt::println("[AssetManager] loadGLTF cache expired key='{}' path='{}' (reloading)", key, resolved);
_gltfCacheByPath.erase(it);
}
fmt::println("[AssetManager] loadGLTF cache expired key='{}' path='{}' (reloading)", key, resolved);
}
auto loaded = loadGltf(_engine, resolved);
auto loaded = loadGltf(_engine, resolved, cb);
if (!loaded.has_value()) return {};
if (loaded.value())
@@ -106,7 +119,10 @@ std::optional<std::shared_ptr<LoadedGLTF> > AssetManager::loadGLTF(std::string_v
fmt::println("[AssetManager] loadGLTF got empty scene for key='{}' path='{}'", key, resolved);
}
_gltfCacheByPath[key] = loaded.value();
{
std::lock_guard<std::mutex> lock(_gltfMutex);
_gltfCacheByPath[key] = loaded.value();
}
return loaded;
}
@@ -336,10 +352,11 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const MeshCreateInfo &info)
return mesh;
}
size_t AssetManager::prefetchGLTFTextures(std::string_view nameOrPath)
AssetManager::GLTFTexturePrefetchResult AssetManager::prefetchGLTFTexturesWithHandles(std::string_view nameOrPath)
{
if (!_engine || !_engine->_context || !_engine->_context->textures) return 0;
if (nameOrPath.empty()) return 0;
GLTFTexturePrefetchResult result{};
if (!_engine || !_engine->_context || !_engine->_context->textures) return result;
if (nameOrPath.empty()) return result;
std::string resolved = assetPath(nameOrPath);
std::filesystem::path path = resolved;
@@ -348,28 +365,28 @@ size_t AssetManager::prefetchGLTFTextures(std::string_view nameOrPath)
constexpr auto gltfOptions = fastgltf::Options::DontRequireValidAssetMember | fastgltf::Options::AllowDouble |
fastgltf::Options::LoadGLBBuffers | fastgltf::Options::LoadExternalBuffers;
fastgltf::GltfDataBuffer data;
if (!data.loadFromFile(path)) return 0;
if (!data.loadFromFile(path)) return result;
fastgltf::Asset gltf;
size_t scheduled = 0;
auto type = fastgltf::determineGltfFileType(&data);
if (type == fastgltf::GltfType::glTF)
{
auto load = parser.loadGLTF(&data, path.parent_path(), gltfOptions);
if (load) gltf = std::move(load.get()); else return 0;
if (load) gltf = std::move(load.get()); else return result;
}
else if (type == fastgltf::GltfType::GLB)
{
auto load = parser.loadBinaryGLTF(&data, path.parent_path(), gltfOptions);
if (load) gltf = std::move(load.get()); else return 0;
if (load) gltf = std::move(load.get()); else return result;
}
else
{
return 0;
return result;
}
TextureCache *cache = _engine->_context->textures;
const std::filesystem::path baseDir = path.parent_path();
auto enqueueTex = [&](size_t imgIndex, bool srgb)
{
@@ -382,10 +399,15 @@ size_t AssetManager::prefetchGLTFTextures(std::string_view nameOrPath)
std::visit(fastgltf::visitor{
[&](fastgltf::sources::URI &filePath)
{
const std::string p(filePath.uri.path().begin(), filePath.uri.path().end());
const std::string rel(filePath.uri.path().begin(), filePath.uri.path().end());
std::filesystem::path resolvedImg = std::filesystem::path(rel);
if (resolvedImg.is_relative())
{
resolvedImg = baseDir / resolvedImg;
}
key.kind = TextureCache::TextureKey::SourceKind::FilePath;
key.path = p;
std::string id = std::string("GLTF-PREF:") + p + (srgb ? "#sRGB" : "#UNORM");
key.path = resolvedImg.string();
std::string id = std::string("GLTF:") + key.path + (srgb ? "#sRGB" : "#UNORM");
key.hash = texcache::fnv1a64(id);
},
[&](fastgltf::sources::Vector &vector)
@@ -418,8 +440,9 @@ size_t AssetManager::prefetchGLTFTextures(std::string_view nameOrPath)
if (key.hash != 0)
{
VkSampler samp = _engine->_samplerManager->defaultLinear();
cache->request(key, samp);
scheduled++;
TextureCache::TextureHandle handle = cache->request(key, samp);
result.handles.push_back(handle);
result.scheduled++;
}
};
@@ -443,7 +466,12 @@ size_t AssetManager::prefetchGLTFTextures(std::string_view nameOrPath)
}, buf.data);
}
return scheduled;
return result;
}
size_t AssetManager::prefetchGLTFTextures(std::string_view nameOrPath)
{
return prefetchGLTFTexturesWithHandles(nameOrPath).scheduled;
}
static Bounds compute_bounds(std::span<Vertex> vertices)

View File

@@ -8,9 +8,11 @@
#include <filesystem>
#include <vector>
#include <utility>
#include <mutex>
#include <scene/vk_loader.h>
#include <core/types.h>
#include <core/assets/texture_cache.h>
#include "render/materials.h"
#include "locator.h"
@@ -82,6 +84,8 @@ public:
std::string assetPath(std::string_view name) const;
std::optional<std::shared_ptr<LoadedGLTF> > loadGLTF(std::string_view nameOrPath);
std::optional<std::shared_ptr<LoadedGLTF> > loadGLTF(std::string_view nameOrPath,
const GLTFLoadCallbacks *cb);
// Queue texture loads for a glTF file ahead of time. This parses the glTF,
// builds TextureCache keys for referenced images (both external URIs and
@@ -89,6 +93,12 @@ public:
// Actual uploads happen via the normal per-frame pump.
// Returns number of textures scheduled.
size_t prefetchGLTFTextures(std::string_view nameOrPath);
struct GLTFTexturePrefetchResult
{
size_t scheduled = 0;
std::vector<TextureCache::TextureHandle> handles;
};
GLTFTexturePrefetchResult prefetchGLTFTexturesWithHandles(std::string_view nameOrPath);
std::shared_ptr<MeshAsset> createMesh(const MeshCreateInfo &info);
@@ -116,6 +126,7 @@ private:
AssetLocator _locator;
std::unordered_map<std::string, std::weak_ptr<LoadedGLTF> > _gltfCacheByPath;
mutable std::mutex _gltfMutex;
std::unordered_map<std::string, std::shared_ptr<MeshAsset> > _meshCache;
std::unordered_map<std::string, AllocatedBuffer> _meshMaterialBuffers;
std::unordered_map<std::string, std::vector<AllocatedImage> > _meshOwnedImages;

View File

@@ -941,3 +941,10 @@ void TextureCache::debug_snapshot(std::vector<DebugRow> &outRows, DebugStats &ou
return a.bytes > b.bytes;
});
}
TextureCache::EntryState TextureCache::state(TextureHandle handle) const
{
if (handle == InvalidHandle) return EntryState::Unloaded;
if (handle >= _entries.size()) return EntryState::Unloaded;
return _entries[handle].state;
}

View File

@@ -42,6 +42,8 @@ public:
using TextureHandle = uint32_t;
static constexpr TextureHandle InvalidHandle = 0xFFFFFFFFu;
enum class EntryState : uint8_t { Unloaded = 0, Loading = 1, Resident = 2, Evicted = 3 };
void init(EngineContext *ctx);
void cleanup();
@@ -83,6 +85,8 @@ public:
size_t countUnloaded{0};
};
void debug_snapshot(std::vector<DebugRow>& outRows, DebugStats& outStats) const;
// Read-only per-handle state query (main-thread only).
EntryState state(TextureHandle handle) const;
size_t resident_bytes() const { return _residentBytes; }
// CPU-side source bytes currently retained (compressed image payloads kept
// for potential re-decode). Only applies to entries created with Bytes keys.
@@ -126,8 +130,6 @@ private:
VkImageView fallbackView{VK_NULL_HANDLE};
};
enum class EntryState : uint8_t { Unloaded, Loading, Resident, Evicted };
struct Entry
{
TextureKey key{};