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

@@ -36,7 +36,15 @@ layout(push_constant) uniform constants
void main() {
// Apply baseColor texture and baseColorFactor once
vec3 albedo = inColor * texture(colorTex, inUV).rgb * materialData.colorFactors.rgb;
vec4 baseTex = texture(colorTex, inUV);
// Alpha from baseColor texture and factor, used for cutouts on MASK materials.
float alpha = clamp(baseTex.a * materialData.colorFactors.a, 0.0, 1.0);
float alphaCutoff = materialData.extra[2].x;
if (alphaCutoff > 0.0 && alpha < alphaCutoff)
{
discard;
}
vec3 albedo = inColor * baseTex.rgb * materialData.colorFactors.rgb;
// glTF metallic-roughness in G (roughness) and B (metallic)
vec2 mrTex = texture(metalRoughTex, inUV).gb;

View File

@@ -17,6 +17,14 @@ void main()
{
// Base color with material factor and texture
vec4 baseTex = texture(colorTex, inUV);
// Alpha from baseColor texture and factor (glTF spec)
float alpha = clamp(baseTex.a * materialData.colorFactors.a, 0.0, 1.0);
// Optional alpha-cutout support for MASK materials (alphaCutoff > 0)
float alphaCutoff = materialData.extra[2].x;
if (alphaCutoff > 0.0 && alpha < alphaCutoff)
{
discard;
}
vec3 albedo = inColor * baseTex.rgb * materialData.colorFactors.rgb;
// glTF: metallicRoughnessTexture uses G=roughness, B=metallic
vec2 mrTex = texture(metalRoughTex, inUV).gb;
@@ -74,7 +82,5 @@ void main()
vec3 indirect = diffIBL + specIBL;
vec3 color = direct + indirect * ao + emissive;
// Alpha from baseColor texture and factor (glTF spec)
float alpha = clamp(baseTex.a * materialData.colorFactors.a, 0.0, 1.0);
outFragColor = vec4(color, alpha);
}

View File

@@ -34,6 +34,8 @@ add_executable (vulkan_engine
core/assets/locator.cpp
core/assets/manager.h
core/assets/manager.cpp
core/assets/async_loader.h
core/assets/async_loader.cpp
core/assets/texture_cache.h
core/assets/texture_cache.cpp
core/assets/ktx_loader.h

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{};

View File

@@ -221,7 +221,10 @@ AllocatedImage ResourceManager::create_image(const void *data, VkExtent3D size,
? static_cast<uint32_t>(std::floor(std::log2(std::max(size.width, size.height)))) + 1
: 1;
_pendingImageUploads.push_back(std::move(pending));
{
std::lock_guard<std::mutex> lk(_pendingMutex);
_pendingImageUploads.push_back(std::move(pending));
}
if (!_deferUploads)
{
@@ -258,7 +261,10 @@ AllocatedImage ResourceManager::create_image(const void *data, VkExtent3D size,
pending.mipLevels = (mipmapped && mipLevelsOverride > 0) ? mipLevelsOverride
: (mipmapped ? static_cast<uint32_t>(std::floor(std::log2(std::max(size.width, size.height)))) + 1 : 1);
_pendingImageUploads.push_back(std::move(pending));
{
std::lock_guard<std::mutex> lk(_pendingMutex);
_pendingImageUploads.push_back(std::move(pending));
}
if (!_deferUploads)
{
@@ -340,7 +346,10 @@ GPUMeshBuffers ResourceManager::uploadMesh(std::span<uint32_t> indices, std::spa
.stagingOffset = vertexBufferSize,
});
_pendingBufferUploads.push_back(std::move(pending));
{
std::lock_guard<std::mutex> lk(_pendingMutex);
_pendingBufferUploads.push_back(std::move(pending));
}
if (!_deferUploads)
{
@@ -352,29 +361,43 @@ GPUMeshBuffers ResourceManager::uploadMesh(std::span<uint32_t> indices, std::spa
bool ResourceManager::has_pending_uploads() const
{
std::lock_guard<std::mutex> lk(_pendingMutex);
return !_pendingBufferUploads.empty() || !_pendingImageUploads.empty();
}
void ResourceManager::clear_pending_uploads()
{
for (auto &upload : _pendingBufferUploads)
std::vector<PendingBufferUpload> buffers;
std::vector<PendingImageUpload> images;
{
std::lock_guard<std::mutex> lk(_pendingMutex);
buffers.swap(_pendingBufferUploads);
images.swap(_pendingImageUploads);
}
for (auto &upload : buffers)
{
destroy_buffer(upload.staging);
}
for (auto &upload : _pendingImageUploads)
for (auto &upload : images)
{
destroy_buffer(upload.staging);
}
_pendingBufferUploads.clear();
_pendingImageUploads.clear();
}
void ResourceManager::process_queued_uploads_immediate()
{
if (!has_pending_uploads()) return;
std::vector<PendingBufferUpload> buffers;
std::vector<PendingImageUpload> images;
{
std::lock_guard<std::mutex> lk(_pendingMutex);
if (_pendingBufferUploads.empty() && _pendingImageUploads.empty()) return;
buffers.swap(_pendingBufferUploads);
images.swap(_pendingImageUploads);
}
immediate_submit([&](VkCommandBuffer cmd) {
for (auto &bufferUpload : _pendingBufferUploads)
for (auto &bufferUpload : buffers)
{
for (const auto &copy : bufferUpload.copies)
{
@@ -386,7 +409,7 @@ void ResourceManager::process_queued_uploads_immediate()
}
}
for (auto &imageUpload : _pendingImageUploads)
for (auto &imageUpload : images)
{
vkutil::transition_image(cmd, imageUpload.image, imageUpload.initialLayout,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
@@ -434,15 +457,26 @@ void ResourceManager::process_queued_uploads_immediate()
}
});
clear_pending_uploads();
for (auto &upload : buffers)
{
destroy_buffer(upload.staging);
}
for (auto &upload : images)
{
destroy_buffer(upload.staging);
}
}
void ResourceManager::register_upload_pass(RenderGraph &graph, FrameResources &frame)
{
if (_pendingBufferUploads.empty() && _pendingImageUploads.empty()) return;
auto bufferUploads = std::make_shared<std::vector<PendingBufferUpload>>(std::move(_pendingBufferUploads));
auto imageUploads = std::make_shared<std::vector<PendingImageUpload>>(std::move(_pendingImageUploads));
std::shared_ptr<std::vector<PendingBufferUpload>> bufferUploads;
std::shared_ptr<std::vector<PendingImageUpload>> imageUploads;
{
std::lock_guard<std::mutex> lk(_pendingMutex);
if (_pendingBufferUploads.empty() && _pendingImageUploads.empty()) return;
bufferUploads = std::make_shared<std::vector<PendingBufferUpload>>(std::move(_pendingBufferUploads));
imageUploads = std::make_shared<std::vector<PendingImageUpload>>(std::move(_pendingImageUploads));
}
struct BufferBinding
{
@@ -680,7 +714,10 @@ AllocatedImage ResourceManager::create_image_compressed(const void* bytes, size_
pending.copies.push_back(region);
}
_pendingImageUploads.push_back(std::move(pending));
{
std::lock_guard<std::mutex> lk(_pendingMutex);
_pendingImageUploads.push_back(std::move(pending));
}
if (!_deferUploads)
{
@@ -756,7 +793,10 @@ AllocatedImage ResourceManager::create_image_compressed_layers(const void* bytes
pending.mipLevels = mipLevels;
pending.copies.assign(regions.begin(), regions.end());
_pendingImageUploads.push_back(std::move(pending));
{
std::lock_guard<std::mutex> lk(_pendingMutex);
_pendingImageUploads.push_back(std::move(pending));
}
if (!_deferUploads)
{

View File

@@ -2,6 +2,7 @@
#include <core/types.h>
#include <functional>
#include <vector>
#include <mutex>
class DeviceManager;
class RenderGraph;
@@ -96,8 +97,6 @@ public:
void immediate_submit(std::function<void(VkCommandBuffer)> &&function) const;
bool has_pending_uploads() const;
const std::vector<PendingBufferUpload> &pending_buffer_uploads() const { return _pendingBufferUploads; }
const std::vector<PendingImageUpload> &pending_image_uploads() const { return _pendingImageUploads; }
void clear_pending_uploads();
void process_queued_uploads_immediate();
@@ -119,4 +118,6 @@ private:
bool _deferUploads = false;
DeletionQueue _deletionQueue;
mutable std::mutex _pendingMutex;
};

View File

@@ -204,6 +204,10 @@ void VulkanEngine::init()
_textureCache->set_max_bytes_per_pump(128ull * 1024ull * 1024ull); // 128 MiB/frame
_textureCache->set_max_upload_dimension(4096);
// Async asset loader for background glTF + texture jobs
_asyncLoader = std::make_unique<AsyncAssetLoader>();
_asyncLoader->init(this, _assetManager.get(), _textureCache.get(), 1);
// Optional ray tracing manager if supported and extensions enabled
if (_deviceManager->supportsRayQuery() && _deviceManager->supportsAccelerationStructure())
{
@@ -375,8 +379,26 @@ bool VulkanEngine::addGLTFInstance(const std::string &instanceName,
return true;
}
uint32_t VulkanEngine::loadGLTFAsync(const std::string &sceneName,
const std::string &modelRelativePath,
const glm::mat4 &transform)
{
if (!_asyncLoader || !_assetManager || !_sceneManager)
{
return 0;
}
return _asyncLoader->load_gltf_async(sceneName, modelRelativePath, transform);
}
void VulkanEngine::cleanup()
{
if (_asyncLoader)
{
_asyncLoader->shutdown();
_asyncLoader.reset();
}
vkDeviceWaitIdle(_deviceManager->device());
print_vma_stats(_deviceManager.get(), "begin");
@@ -481,6 +503,12 @@ void VulkanEngine::cleanup()
void VulkanEngine::draw()
{
// Integrate any completed async asset jobs into the scene before updating.
if (_asyncLoader && _sceneManager)
{
_asyncLoader->pump_main_thread(*_sceneManager);
}
_sceneManager->update_scene();
// Update IBL based on camera position and user-defined reflection volumes.

View File

@@ -30,6 +30,7 @@
#include "core/context.h"
#include "core/pipeline/manager.h"
#include "core/assets/manager.h"
#include "core/assets/async_loader.h"
#include "render/graph/graph.h"
#include "core/raytracing/raytracing.h"
#include "core/assets/texture_cache.h"
@@ -70,6 +71,7 @@ public:
std::unique_ptr<SceneManager> _sceneManager;
std::unique_ptr<PipelineManager> _pipelineManager;
std::unique_ptr<AssetManager> _assetManager;
std::unique_ptr<AsyncAssetLoader> _asyncLoader;
std::unique_ptr<RenderGraph> _renderGraph;
std::unique_ptr<RayTracingManager> _rayManager;
std::unique_ptr<TextureCache> _textureCache;
@@ -202,6 +204,12 @@ public:
const std::string &modelRelativePath,
const glm::mat4 &transform = glm::mat4(1.f));
// Asynchronous glTF load that reports progress via AsyncAssetLoader.
// Returns a JobID that can be queried via AsyncAssetLoader.
uint32_t loadGLTFAsync(const std::string &sceneName,
const std::string &modelRelativePath,
const glm::mat4 &transform = glm::mat4(1.f));
bool resize_requested{false};
bool freeze_rendering{false};

View File

@@ -379,6 +379,97 @@ namespace
}
}
static const char *job_state_name(AsyncAssetLoader::JobState s)
{
using JS = AsyncAssetLoader::JobState;
switch (s)
{
case JS::Pending: return "Pending";
case JS::Running: return "Running";
case JS::Completed: return "Completed";
case JS::Failed: return "Failed";
case JS::Cancelled: return "Cancelled";
default: return "?";
}
}
static void ui_async_assets(VulkanEngine *eng)
{
if (!eng || !eng->_asyncLoader)
{
ImGui::TextUnformatted("AsyncAssetLoader not available");
return;
}
std::vector<AsyncAssetLoader::DebugJob> jobs;
eng->_asyncLoader->debug_snapshot(jobs);
ImGui::Text("Active jobs: %zu", jobs.size());
ImGui::Separator();
if (!jobs.empty())
{
if (ImGui::BeginTable("async_jobs", 5, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Scene");
ImGui::TableSetupColumn("Model");
ImGui::TableSetupColumn("State", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("Progress", ImGuiTableColumnFlags_WidthFixed, 180);
ImGui::TableHeadersRow();
for (const auto &j : jobs)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("%u", j.id);
ImGui::TableSetColumnIndex(1);
ImGui::TextUnformatted(j.scene_name.c_str());
ImGui::TableSetColumnIndex(2);
ImGui::TextUnformatted(j.model_relative_path.c_str());
ImGui::TableSetColumnIndex(3);
ImGui::TextUnformatted(job_state_name(j.state));
ImGui::TableSetColumnIndex(4);
float p = j.progress;
ImGui::ProgressBar(p, ImVec2(-FLT_MIN, 0.0f));
if (j.texture_count > 0)
{
ImGui::SameLine();
ImGui::Text("(%zu/%zu tex)", j.textures_resident, j.texture_count);
}
}
ImGui::EndTable();
}
}
else
{
ImGui::TextUnformatted("No async asset jobs currently running.");
}
ImGui::Separator();
ImGui::TextUnformatted("Spawn async glTF instance");
static char gltfPath[256] = "mirage2000/scene.gltf";
static char gltfName[128] = "async_gltf_01";
static float gltfPos[3] = {0.0f, 0.0f, 0.0f};
static float gltfRot[3] = {0.0f, 0.0f, 0.0f};
static float gltfScale[3] = {1.0f, 1.0f, 1.0f};
ImGui::InputText("Model path (assets/models/...)", gltfPath, IM_ARRAYSIZE(gltfPath));
ImGui::InputText("Instance name", gltfName, IM_ARRAYSIZE(gltfName));
ImGui::InputFloat3("Position", gltfPos);
ImGui::InputFloat3("Rotation (deg XYZ)", gltfRot);
ImGui::InputFloat3("Scale", gltfScale);
if (ImGui::Button("Load glTF async"))
{
glm::mat4 T = glm::translate(glm::mat4(1.0f), glm::vec3(gltfPos[0], gltfPos[1], gltfPos[2]));
glm::mat4 R = glm::eulerAngleXYZ(glm::radians(gltfRot[0]),
glm::radians(gltfRot[1]),
glm::radians(gltfRot[2]));
glm::mat4 S = glm::scale(glm::mat4(1.0f), glm::vec3(gltfScale[0], gltfScale[1], gltfScale[2]));
glm::mat4 M = T * R * S;
eng->loadGLTFAsync(gltfName, gltfPath, M);
}
}
// Shadows / Ray Query controls
static void ui_shadows(VulkanEngine *eng)
{
@@ -1164,6 +1255,11 @@ void vk_engine_draw_debug_ui(VulkanEngine *eng)
ui_scene(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Async Assets"))
{
ui_async_assets(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Textures"))
{
ui_textures(eng);

View File

@@ -161,7 +161,9 @@ VkSamplerMipmapMode extract_mipmap_mode(fastgltf::Filter filter)
//< filters
std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::string_view filePath)
std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine,
std::string_view filePath,
const GLTFLoadCallbacks *cb)
{
//> load_1
fmt::println("[GLTF] loadGltf begin: '{}'", filePath);
@@ -216,6 +218,24 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
return {};
}
//< load_1
// Simple helpers for progress/cancellation callbacks (if provided)
auto report_progress = [&](float v)
{
if (cb && cb->on_progress)
{
float clamped = std::clamp(v, 0.0f, 1.0f);
cb->on_progress(clamped);
}
};
auto is_cancelled = [&]() -> bool
{
if (cb && cb->is_cancelled)
{
return cb->is_cancelled();
}
return false;
};
//> load_2
// we can stimate the descriptors we will need accurately
fmt::println("[GLTF] loadGltf: materials={} meshes={} images={} samplers={} (creating descriptor pool)",
@@ -235,6 +255,8 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
fmt::println("[GLTF] loadGltf: descriptor pool initialized for '{}' (materials={})",
filePath,
gltf.materials.size());
report_progress(0.1f);
//< load_2
//> load_samplers
@@ -270,6 +292,8 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
file.samplers.push_back(newSampler);
}
//< load_samplers
report_progress(0.2f);
//> load_arrays
// temporal arrays for all the objects to use while creating the GLTF data
std::vector<std::shared_ptr<MeshAsset> > meshes;
@@ -349,6 +373,8 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
//> load_material
for (fastgltf::Material &mat: gltf.materials)
{
if (is_cancelled()) return {};
std::shared_ptr<GLTFMaterial> newMat = std::make_shared<GLTFMaterial>();
materials.push_back(newMat);
file.materials[mat.name.c_str()] = newMat;
@@ -361,11 +387,20 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
constants.metal_rough_factors.x = mat.pbrData.metallicFactor;
constants.metal_rough_factors.y = mat.pbrData.roughnessFactor;
// extra[0].x: normalScale (default 1.0)
constants.extra[0].x = 1.0f;
// extra[0].y: occlusionStrength (0..1, default 1.0)
constants.extra[0].y = mat.occlusionTexture.has_value() ? mat.occlusionTexture->strength : 1.0f;
// extra[1].rgb: emissiveFactor
constants.extra[1].x = mat.emissiveFactor[0];
constants.extra[1].y = mat.emissiveFactor[1];
constants.extra[1].z = mat.emissiveFactor[2];
// extra[2].x: alphaCutoff for MASK materials (>0 enables alpha test)
constants.extra[2].x = 0.0f;
if (mat.alphaMode == fastgltf::AlphaMode::Mask)
{
constants.extra[2].x = static_cast<float>(mat.alphaCutoff);
}
// write material parameters to buffer
sceneMaterialConstants[data_index] = constants;
@@ -510,6 +545,12 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
}
//< load_material
// Rough progress after materials and texture requests
if (!gltf.meshes.empty())
{
report_progress(0.25f);
}
// Flush material constants buffer so GPU sees updated data on non-coherent memory
if (!gltf.materials.empty())
{
@@ -522,8 +563,11 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
std::vector<uint32_t> indices;
std::vector<Vertex> vertices;
for (fastgltf::Mesh &mesh: gltf.meshes)
for (size_t meshIndex = 0; meshIndex < gltf.meshes.size(); ++meshIndex)
{
if (is_cancelled()) return {};
fastgltf::Mesh &mesh = gltf.meshes[meshIndex];
std::shared_ptr<MeshAsset> newmesh = std::make_shared<MeshAsset>();
meshes.push_back(newmesh);
file.meshes[mesh.name.c_str()] = newmesh;
@@ -704,11 +748,18 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
};
shrink_if_huge(indices, sizeof(uint32_t));
shrink_if_huge(vertices, sizeof(Vertex));
// Update progress based on meshes built so far; meshes/BVH/uploads get 0.6 of the range.
float meshFrac = static_cast<float>(meshIndex + 1) / static_cast<float>(gltf.meshes.size());
report_progress(0.2f + meshFrac * 0.6f);
}
//> load_nodes
// load all nodes and their meshes
for (fastgltf::Node &node: gltf.nodes)
for (size_t nodeIndex = 0; nodeIndex < gltf.nodes.size(); ++nodeIndex)
{
if (is_cancelled()) return {};
fastgltf::Node &node = gltf.nodes[nodeIndex];
std::shared_ptr<Node> newNode;
// find if the node has a mesh, and if it does hook it to the mesh pointer and allocate it with the meshnode class
@@ -752,6 +803,14 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
}
},
node.transform);
// Node building and hierarchy wiring shares a small slice of progress.
if (!gltf.nodes.empty())
{
float nodeFrac = static_cast<float>(nodeIndex + 1) / static_cast<float>(gltf.nodes.size());
// Reserve 0.1 of the total range for nodes/animations/transforms.
report_progress(0.8f + nodeFrac * 0.1f);
}
}
//< load_nodes
//> load_graph
@@ -892,6 +951,8 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
// LoadedGLTF only stores shared animation clips.
}
report_progress(0.95f);
// We no longer need glTF-owned buffer payloads; free any large vectors
for (auto &buf : gltf.buffers)
{
@@ -918,6 +979,8 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
file.samplers.size(),
file.animations.size(),
file.debugName.empty() ? "<none>" : file.debugName);
report_progress(1.0f);
return scene;
//< load_graph
}

View File

@@ -8,6 +8,7 @@
#include "core/descriptor/descriptors.h"
#include <unordered_map>
#include <filesystem>
#include <functional>
class VulkanEngine;
@@ -59,6 +60,12 @@ struct MeshAsset
std::shared_ptr<MeshBVH> bvh;
};
struct GLTFLoadCallbacks
{
std::function<void(float)> on_progress; // range 0..1
std::function<bool()> is_cancelled; // optional, may be null
};
struct LoadedGLTF : public IRenderable
{
// storage for all the data on a given gltf file
@@ -133,4 +140,6 @@ private:
void clearAll();
};
std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::string_view filePath);
std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine,
std::string_view filePath,
const GLTFLoadCallbacks *cb = nullptr);