ADD: async glTF loading
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
298
src/core/assets/async_loader.cpp
Normal file
298
src/core/assets/async_loader.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
97
src/core/assets/async_loader.h
Normal file
97
src/core/assets/async_loader.h
Normal 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};
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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 © : 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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user