diff --git a/shaders/gbuffer.frag b/shaders/gbuffer.frag index 8dff01a..c82a4a1 100644 --- a/shaders/gbuffer.frag +++ b/shaders/gbuffer.frag @@ -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; diff --git a/shaders/mesh.frag b/shaders/mesh.frag index b6565c7..707d4bc 100644 --- a/shaders/mesh.frag +++ b/shaders/mesh.frag @@ -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); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3e9a3e1..0dfc1d4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/core/assets/async_loader.cpp b/src/core/assets/async_loader.cpp new file mode 100644 index 0000000..e8926ae --- /dev/null +++ b/src/core/assets/async_loader.cpp @@ -0,0 +1,298 @@ +#include "async_loader.h" + +#include + +#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 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 = std::make_unique(/*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 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 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(resident) / static_cast(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 &out_jobs) +{ + out_jobs.clear(); + + std::lock_guard 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(tex_resident) / static_cast(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 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 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 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 > 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); + } +} diff --git a/src/core/assets/async_loader.h b/src/core/assets/async_loader.h new file mode 100644 index 0000000..2e82d08 --- /dev/null +++ b/src/core/assets/async_loader.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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 &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 scene; + + std::atomic progress{0.0f}; + std::atomic state{JobState::Pending}; + + std::string error; + bool committed_to_scene{false}; + + // Texture handles associated with this glTF (prefetched via TextureCache). + std::vector 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 _running{false}; + std::vector _workers; + + std::mutex _jobs_mutex; + std::condition_variable _jobs_cv; + std::unordered_map> _jobs; + std::deque _queue; + std::atomic _next_id{1}; +}; diff --git a/src/core/assets/manager.cpp b/src/core/assets/manager.cpp index 7eac8a4..3d67e3b 100644 --- a/src/core/assets/manager.cpp +++ b/src/core/assets/manager.cpp @@ -52,7 +52,10 @@ void AssetManager::cleanup() _meshCache.clear(); _meshMaterialBuffers.clear(); _meshOwnedImages.clear(); - _gltfCacheByPath.clear(); + { + std::lock_guard 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 > AssetManager::loadGLTF(std::string_view nameOrPath) +{ + return loadGLTF(nameOrPath, nullptr); +} + +std::optional > AssetManager::loadGLTF(std::string_view nameOrPath, + const GLTFLoadCallbacks *cb) { if (!_engine) return {}; if (nameOrPath.empty()) return {}; @@ -82,18 +91,22 @@ std::optional > 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 lock(_gltfMutex); + if (auto it = _gltfCacheByPath.find(key); it != _gltfCacheByPath.end()) { - fmt::println("[AssetManager] loadGLTF cache hit key='{}' path='{}' ptr={}", key, resolved, - static_cast(sp.get())); - return sp; + if (auto sp = it->second.lock()) + { + fmt::println("[AssetManager] loadGLTF cache hit key='{}' path='{}' ptr={}", key, resolved, + static_cast(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 > AssetManager::loadGLTF(std::string_v fmt::println("[AssetManager] loadGLTF got empty scene for key='{}' path='{}'", key, resolved); } - _gltfCacheByPath[key] = loaded.value(); + { + std::lock_guard lock(_gltfMutex); + _gltfCacheByPath[key] = loaded.value(); + } return loaded; } @@ -336,10 +352,11 @@ std::shared_ptr 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 vertices) diff --git a/src/core/assets/manager.h b/src/core/assets/manager.h index 95929f7..42e0b7e 100644 --- a/src/core/assets/manager.h +++ b/src/core/assets/manager.h @@ -8,9 +8,11 @@ #include #include #include +#include #include #include +#include #include "render/materials.h" #include "locator.h" @@ -82,6 +84,8 @@ public: std::string assetPath(std::string_view name) const; std::optional > loadGLTF(std::string_view nameOrPath); + std::optional > 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 handles; + }; + GLTFTexturePrefetchResult prefetchGLTFTexturesWithHandles(std::string_view nameOrPath); std::shared_ptr createMesh(const MeshCreateInfo &info); @@ -116,6 +126,7 @@ private: AssetLocator _locator; std::unordered_map > _gltfCacheByPath; + mutable std::mutex _gltfMutex; std::unordered_map > _meshCache; std::unordered_map _meshMaterialBuffers; std::unordered_map > _meshOwnedImages; diff --git a/src/core/assets/texture_cache.cpp b/src/core/assets/texture_cache.cpp index 127c89a..ee8d922 100644 --- a/src/core/assets/texture_cache.cpp +++ b/src/core/assets/texture_cache.cpp @@ -941,3 +941,10 @@ void TextureCache::debug_snapshot(std::vector &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; +} diff --git a/src/core/assets/texture_cache.h b/src/core/assets/texture_cache.h index 089fddd..06bd84f 100644 --- a/src/core/assets/texture_cache.h +++ b/src/core/assets/texture_cache.h @@ -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& 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{}; diff --git a/src/core/device/resource.cpp b/src/core/device/resource.cpp index 48cbeeb..889d275 100644 --- a/src/core/device/resource.cpp +++ b/src/core/device/resource.cpp @@ -221,7 +221,10 @@ AllocatedImage ResourceManager::create_image(const void *data, VkExtent3D size, ? static_cast(std::floor(std::log2(std::max(size.width, size.height)))) + 1 : 1; - _pendingImageUploads.push_back(std::move(pending)); + { + std::lock_guard 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(std::floor(std::log2(std::max(size.width, size.height)))) + 1 : 1); - _pendingImageUploads.push_back(std::move(pending)); + { + std::lock_guard lk(_pendingMutex); + _pendingImageUploads.push_back(std::move(pending)); + } if (!_deferUploads) { @@ -340,7 +346,10 @@ GPUMeshBuffers ResourceManager::uploadMesh(std::span indices, std::spa .stagingOffset = vertexBufferSize, }); - _pendingBufferUploads.push_back(std::move(pending)); + { + std::lock_guard lk(_pendingMutex); + _pendingBufferUploads.push_back(std::move(pending)); + } if (!_deferUploads) { @@ -352,29 +361,43 @@ GPUMeshBuffers ResourceManager::uploadMesh(std::span indices, std::spa bool ResourceManager::has_pending_uploads() const { + std::lock_guard lk(_pendingMutex); return !_pendingBufferUploads.empty() || !_pendingImageUploads.empty(); } void ResourceManager::clear_pending_uploads() { - for (auto &upload : _pendingBufferUploads) + std::vector buffers; + std::vector images; + { + std::lock_guard 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 buffers; + std::vector images; + { + std::lock_guard 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::move(_pendingBufferUploads)); - auto imageUploads = std::make_shared>(std::move(_pendingImageUploads)); + std::shared_ptr> bufferUploads; + std::shared_ptr> imageUploads; + { + std::lock_guard lk(_pendingMutex); + if (_pendingBufferUploads.empty() && _pendingImageUploads.empty()) return; + bufferUploads = std::make_shared>(std::move(_pendingBufferUploads)); + imageUploads = std::make_shared>(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 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 lk(_pendingMutex); + _pendingImageUploads.push_back(std::move(pending)); + } if (!_deferUploads) { diff --git a/src/core/device/resource.h b/src/core/device/resource.h index 6e67eaf..61b6d77 100644 --- a/src/core/device/resource.h +++ b/src/core/device/resource.h @@ -2,6 +2,7 @@ #include #include #include +#include class DeviceManager; class RenderGraph; @@ -96,8 +97,6 @@ public: void immediate_submit(std::function &&function) const; bool has_pending_uploads() const; - const std::vector &pending_buffer_uploads() const { return _pendingBufferUploads; } - const std::vector &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; }; diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 33a4342..dc10928 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -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(); + _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. diff --git a/src/core/engine.h b/src/core/engine.h index 4539ac7..fc3078e 100644 --- a/src/core/engine.h +++ b/src/core/engine.h @@ -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; std::unique_ptr _pipelineManager; std::unique_ptr _assetManager; + std::unique_ptr _asyncLoader; std::unique_ptr _renderGraph; std::unique_ptr _rayManager; std::unique_ptr _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}; diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index 8fa292c..a82b320 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -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 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); diff --git a/src/scene/vk_loader.cpp b/src/scene/vk_loader.cpp index df642be..5fc450b 100644 --- a/src/scene/vk_loader.cpp +++ b/src/scene/vk_loader.cpp @@ -161,7 +161,9 @@ VkSamplerMipmapMode extract_mipmap_mode(fastgltf::Filter filter) //< filters -std::optional > loadGltf(VulkanEngine *engine, std::string_view filePath) +std::optional > loadGltf(VulkanEngine *engine, + std::string_view filePath, + const GLTFLoadCallbacks *cb) { //> load_1 fmt::println("[GLTF] loadGltf begin: '{}'", filePath); @@ -216,6 +218,24 @@ std::optional > 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 > 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 > 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 > meshes; @@ -349,6 +373,8 @@ std::optional > loadGltf(VulkanEngine *engine, std:: //> load_material for (fastgltf::Material &mat: gltf.materials) { + if (is_cancelled()) return {}; + std::shared_ptr newMat = std::make_shared(); materials.push_back(newMat); file.materials[mat.name.c_str()] = newMat; @@ -361,11 +387,20 @@ std::optional > 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(mat.alphaCutoff); + } // write material parameters to buffer sceneMaterialConstants[data_index] = constants; @@ -510,6 +545,12 @@ std::optional > 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 > loadGltf(VulkanEngine *engine, std:: std::vector indices; std::vector 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 newmesh = std::make_shared(); meshes.push_back(newmesh); file.meshes[mesh.name.c_str()] = newmesh; @@ -704,11 +748,18 @@ std::optional > 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(meshIndex + 1) / static_cast(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 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 > 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(nodeIndex + 1) / static_cast(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 > 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 > loadGltf(VulkanEngine *engine, std:: file.samplers.size(), file.animations.size(), file.debugName.empty() ? "" : file.debugName); + + report_progress(1.0f); return scene; //< load_graph } diff --git a/src/scene/vk_loader.h b/src/scene/vk_loader.h index 1e87082..8cd67aa 100644 --- a/src/scene/vk_loader.h +++ b/src/scene/vk_loader.h @@ -8,6 +8,7 @@ #include "core/descriptor/descriptors.h" #include #include +#include class VulkanEngine; @@ -59,6 +60,12 @@ struct MeshAsset std::shared_ptr bvh; }; +struct GLTFLoadCallbacks +{ + std::function on_progress; // range 0..1 + std::function 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 > loadGltf(VulkanEngine *engine, std::string_view filePath); +std::optional > loadGltf(VulkanEngine *engine, + std::string_view filePath, + const GLTFLoadCallbacks *cb = nullptr);