From 2d2d4bd00724fd1fe4fcee5803c802fb214b045c Mon Sep 17 00:00:00 2001 From: Bosheng Li Date: Thu, 4 Jun 2026 14:05:50 -0700 Subject: [PATCH 1/3] Add GPU-ready asset upload staging --- EvoEngine_SDK/include/Core/AssetManager.hpp | 12 +- EvoEngine_SDK/include/Core/ECS/IAsset.hpp | 31 +++ EvoEngine_SDK/include/Layers/RenderLayer.hpp | 6 + .../Rendering/Geometry/GeometryStorage.hpp | 63 ++++- .../Rendering/Texture/TextureStorage.hpp | 22 ++ EvoEngine_SDK/src/AssetManager.cpp | 67 ++++- EvoEngine_SDK/src/GeometryStorage.cpp | 251 +++++++++++++++--- EvoEngine_SDK/src/GraphicsResources.cpp | 37 ++- EvoEngine_SDK/src/IAsset.cpp | 51 ++++ EvoEngine_SDK/src/ProjectManager.cpp | 4 +- EvoEngine_SDK/src/RenderInstanceStorage.cpp | 76 ++++-- EvoEngine_SDK/src/RenderLayer.cpp | 14 + EvoEngine_SDK/src/Resources.cpp | 12 + EvoEngine_SDK/src/TaskRuntime.cpp | 5 +- EvoEngine_SDK/src/Texture2D.cpp | 24 +- EvoEngine_SDK/src/TextureStorage.cpp | 125 ++++++++- EvoEngine_Tests/Core/AssetManagerTest.cpp | 227 ++++++++++++++++ EvoEngine_Tests/Core/GpuServiceTest.cpp | 120 +++++++++ EvoEngine_Tests/Core/TaskRuntimeTest.cpp | 11 + PythonBinding/src/PyEvoEngine.cpp | 44 ++- README.md | 4 +- 21 files changed, 1111 insertions(+), 95 deletions(-) diff --git a/EvoEngine_SDK/include/Core/AssetManager.hpp b/EvoEngine_SDK/include/Core/AssetManager.hpp index e68af5ca..d9a84908 100644 --- a/EvoEngine_SDK/include/Core/AssetManager.hpp +++ b/EvoEngine_SDK/include/Core/AssetManager.hpp @@ -22,7 +22,16 @@ class IAsset; */ class AssetManager { public: - enum class AssetLoadState { Discovered, Queued, LoadingCpu, WaitingForFinalize, Loaded, Failed, Cancelled }; + enum class AssetLoadState { + Discovered, + Queued, + LoadingCpu, + WaitingForFinalize, + GpuPending, + Loaded, + Failed, + Cancelled + }; struct AssetLoadSnapshot { size_t total = 0; @@ -32,6 +41,7 @@ class AssetManager { size_t queued = 0; size_t loading_cpu = 0; size_t waiting_for_finalize = 0; + size_t gpu_pending = 0; Handle active_asset_handle = 0; AssetLoadState active_state = AssetLoadState::Discovered; std::string active_asset_name; diff --git a/EvoEngine_SDK/include/Core/ECS/IAsset.hpp b/EvoEngine_SDK/include/Core/ECS/IAsset.hpp index 16a9cb6e..7fc432d0 100644 --- a/EvoEngine_SDK/include/Core/ECS/IAsset.hpp +++ b/EvoEngine_SDK/include/Core/ECS/IAsset.hpp @@ -2,6 +2,11 @@ #pragma once #include "IHandle.hpp" #include "ISerializable.hpp" +#include "Jobs.hpp" + +#include +#include +#include namespace evo_engine { @@ -29,8 +34,14 @@ class StagedAssetLoadPayload { * deserialization, and interactions with the asset's file system and the editor. */ class IAsset : public ISerializable { + struct PendingGpuWorkState { + mutable std::mutex mutex; + std::vector handles; + }; + std::weak_ptr self_; /**< Weak reference to the current IAsset instance. Used internally for managing self-references. */ + std::shared_ptr pending_gpu_work_state_ = std::make_shared(); protected: /** @cond DOXYGEN_SHOULD_SKIP_THIS */ @@ -87,10 +98,30 @@ class IAsset : public ISerializable { virtual bool ApplyStagedPayloadInternal(const std::filesystem::path& path, const std::shared_ptr& payload); + /** + * @brief Tracks asynchronous GPU work that must complete before this asset is fully ready. + */ + void TrackPendingGpuWork(const JobHandle& handle); + bool saved_ = false; /**< Indicates whether the asset is in a saved state. */ uint32_t version_ = 0; /**< The version number of the asset. */ public: + /** + * @brief Returns whether this asset has valid tracked GPU work that has not been consumed by a readiness wait. + */ + [[nodiscard]] bool HasPendingGpuWork() const; + + /** + * @brief Returns tracked GPU work handles for readiness orchestration. + */ + [[nodiscard]] std::vector GetPendingGpuWorkHandles() const; + + /** + * @brief Waits for all tracked GPU work and clears completed readiness handles. + */ + void WaitForPendingGpuWork() const; + /** * @brief Generates a thumbnail texture for the asset. * @return A shared pointer to the generated thumbnail texture. diff --git a/EvoEngine_SDK/include/Layers/RenderLayer.hpp b/EvoEngine_SDK/include/Layers/RenderLayer.hpp index 47061212..68a6af90 100644 --- a/EvoEngine_SDK/include/Layers/RenderLayer.hpp +++ b/EvoEngine_SDK/include/Layers/RenderLayer.hpp @@ -301,6 +301,12 @@ class RenderLayer final : public ILayer { * \brief Applies all animators associated with this render layer. */ void ApplyAnimators() const; + + /** + * \brief Drains GPU work before render-layer resources are released. + */ + void OnDestroy() override; + friend class TextureStorage; std::vector> per_frame_descriptor_sets_ = {}; std::vector> meshlet_descriptor_sets_ = {}; diff --git a/EvoEngine_SDK/include/Rendering/Geometry/GeometryStorage.hpp b/EvoEngine_SDK/include/Rendering/Geometry/GeometryStorage.hpp index bb3b0263..bef5239c 100644 --- a/EvoEngine_SDK/include/Rendering/Geometry/GeometryStorage.hpp +++ b/EvoEngine_SDK/include/Rendering/Geometry/GeometryStorage.hpp @@ -125,7 +125,7 @@ class RangeDescriptor { /** * @brief Offset value of the range descriptor. */ - uint32_t offset; + uint32_t offset = 0; /** * @brief Range value for the descriptor. @@ -133,22 +133,27 @@ class RangeDescriptor { * - When used for meshlets: Represents the count of meshlets for this geometry. * - When used for triangles: Represents the count of triangles, including space for empty fillers. */ - uint32_t range; + uint32_t range = 0; /** * @brief Offset for the previous frame's data. */ - uint32_t prev_frame_offset; + uint32_t prev_frame_offset = 0; /** * @brief Number of indices in the current frame. */ - uint32_t index_count; + uint32_t index_count = 0; /** * @brief Number of indices in the previous frame. */ - uint32_t prev_frame_index_count; + uint32_t prev_frame_index_count = 0; + + /** + * @brief Committed range count that is safe for rendering. + */ + uint32_t prev_frame_range = 0; }; /** @@ -208,8 +213,40 @@ struct ParticleInfoListData { class GeometryStorage final { public: static GeometryStorage& GetInstance(); + struct GeometryUploadSnapshot { + bool mesh_dirty = false; + bool mesh_upload_active = false; + bool mesh_upload_completed = true; + size_t mesh_upload_handles = 0; + + bool skinned_mesh_dirty = false; + bool skinned_mesh_upload_active = false; + bool skinned_mesh_upload_completed = true; + size_t skinned_mesh_upload_handles = 0; + + bool strand_dirty = false; + bool strand_upload_active = false; + bool strand_upload_completed = true; + size_t strand_upload_handles = 0; + + [[nodiscard]] bool Active() const; + }; private: + struct RangeCommit { + std::shared_ptr descriptor; + uint32_t offset = 0; + uint32_t range = 0; + uint32_t index_count = 0; + }; + + struct PendingGeometryUpload { + bool active = false; + std::vector handles; + std::vector meshlet_commits; + std::vector index_commits; + }; + std::vector vertex_data_chunks_ = {}; std::vector meshlets_ = {}; std::vector> meshlet_range_descriptor_; @@ -220,6 +257,7 @@ class GeometryStorage final { std::shared_ptr meshlet_buffer_ = {}; std::shared_ptr triangle_buffer_ = {}; bool require_mesh_data_device_update_ = {}; + PendingGeometryUpload pending_mesh_upload_; std::vector skinned_vertex_data_chunks_ = {}; std::vector skinned_meshlets_ = {}; @@ -231,6 +269,7 @@ class GeometryStorage final { std::shared_ptr skinned_meshlet_buffer_ = {}; std::shared_ptr skinned_triangle_buffer_ = {}; bool require_skinned_mesh_data_device_update_ = {}; + PendingGeometryUpload pending_skinned_mesh_upload_; std::vector strand_point_data_chunks_ = {}; std::vector strand_meshlets_ = {}; @@ -242,8 +281,19 @@ class GeometryStorage final { std::shared_ptr strand_meshlet_buffer_ = {}; std::shared_ptr segment_buffer_ = {}; bool require_strand_mesh_data_device_update_ = {}; + PendingGeometryUpload pending_strand_upload_; void UploadData(); + static void CaptureRangeCommits(const std::vector>& descriptors, + std::vector& commits); + static void ApplyRangeCommits(const std::vector& commits); + static bool HasValidUploadHandle(const PendingGeometryUpload& upload); + static bool IsPendingUploadCompleted(const PendingGeometryUpload& upload); + static void WaitPendingUpload(PendingGeometryUpload& upload); + static void ClearPendingUpload(PendingGeometryUpload& upload); + bool CompletePendingUpload(PendingGeometryUpload& upload); + void CompletePendingUploads(); + void SchedulePendingUploads(); friend class RenderLayer; friend class Resources; friend class Platform; @@ -256,6 +306,9 @@ class GeometryStorage final { public: [[nodiscard]] static uint32_t GetVersion(); + [[nodiscard]] static GeometryUploadSnapshot GetUploadSnapshot(); + [[nodiscard]] static bool HasPendingUploads(); + static void WaitForPendingUploads(); static const std::shared_ptr& GetTriangleBuffer(); static const std::shared_ptr& GetVertexBuffer(); static const std::shared_ptr& GetMeshletBuffer(); diff --git a/EvoEngine_SDK/include/Rendering/Texture/TextureStorage.hpp b/EvoEngine_SDK/include/Rendering/Texture/TextureStorage.hpp index a5b04515..88663012 100644 --- a/EvoEngine_SDK/include/Rendering/Texture/TextureStorage.hpp +++ b/EvoEngine_SDK/include/Rendering/Texture/TextureStorage.hpp @@ -2,6 +2,8 @@ #pragma once #include "GraphicsResources.hpp" +#include + namespace evo_engine { /** @@ -47,6 +49,8 @@ class Texture2DStorage { std::shared_ptr sampler = {}; ///< GPU sampler resource. ImTextureID im_texture_id = 0; ///< ImGui texture ID for rendering. + std::shared_ptr gpu_upload_in_flight = + std::make_shared(0); ///< Async GPU uploads currently mutating image state. /** * @brief Retrieves the Vulkan image layout of the texture. @@ -78,6 +82,11 @@ class Texture2DStorage { */ [[nodiscard]] std::shared_ptr GetImage() const; + /** + * @brief Returns whether an asynchronous GPU upload is still pending. + */ + [[nodiscard]] bool IsGpuUploadPending() const; + /** * @brief Initializes the GPU resources for the texture with the given resolution. * @param resolution The resolution of the texture. @@ -91,6 +100,14 @@ class Texture2DStorage { */ void SetDataImmediately(const std::vector& data, const glm::uvec2& resolution); + /** + * @brief Sets texture data and uploads it asynchronously through the GPU service. + * @param data The pixel data to upload. + * @param resolution The resolution of the texture. + * @return A handle that completes when the GPU upload finishes. + */ + [[nodiscard]] GpuWorkHandle SetDataAsync(const std::vector& data, const glm::uvec2& resolution); + /** * @brief Sets texture data and queues it for upload during a batch process. * @param data The pixel data to set. @@ -202,6 +219,11 @@ class TextureStorage final { */ [[nodiscard]] static uint32_t GetVersion(); + /** + * @brief Returns whether any texture storage still has queued or in-flight GPU upload work. + */ + [[nodiscard]] static bool HasPendingUploads(); + /** * @brief Synchronizes the device to ensure all texture-related operations are complete. */ diff --git a/EvoEngine_SDK/src/AssetManager.cpp b/EvoEngine_SDK/src/AssetManager.cpp index 2a06bf7b..d57607cc 100644 --- a/EvoEngine_SDK/src/AssetManager.cpp +++ b/EvoEngine_SDK/src/AssetManager.cpp @@ -27,6 +27,8 @@ std::string AssetLoadStateName(const AssetManager::AssetLoadState state) { return "Loading CPU payload"; case AssetManager::AssetLoadState::WaitingForFinalize: return "Waiting for finalization"; + case AssetManager::AssetLoadState::GpuPending: + return "Waiting for GPU finalization"; case AssetManager::AssetLoadState::Loaded: return "Loaded"; case AssetManager::AssetLoadState::Failed: @@ -40,7 +42,7 @@ std::string AssetLoadStateName(const AssetManager::AssetLoadState state) { } // namespace bool AssetManager::AssetLoadSnapshot::Active() const { - return queued != 0 || loading_cpu != 0 || waiting_for_finalize != 0; + return queued != 0 || loading_cpu != 0 || waiting_for_finalize != 0 || gpu_pending != 0; } void AssetManager::Initialize() { @@ -234,6 +236,7 @@ AssetManager::AssetLoadSnapshot AssetManager::GetAssetLoadSnapshot() { snapshot.queued = 0; snapshot.loading_cpu = 0; snapshot.waiting_for_finalize = 0; + snapshot.gpu_pending = 0; for (const auto& [handle, record] : asset_manager.asset_registry_.loading_assets_) { switch (record.state) { case AssetLoadState::Queued: @@ -245,6 +248,9 @@ AssetManager::AssetLoadSnapshot AssetManager::GetAssetLoadSnapshot() { case AssetLoadState::WaitingForFinalize: ++snapshot.waiting_for_finalize; break; + case AssetLoadState::GpuPending: + ++snapshot.gpu_pending; + break; default: break; } @@ -405,22 +411,65 @@ void AssetManager::StartAssetServiceLoadImpl(const Handle& asset_handle, throw std::runtime_error("Failed to apply staged asset load payload."); } context.asset->saved_ = true; - context.file->asset_ = context.asset; - { - auto& asset_manager = GetInstance(); - std::lock_guard lock(asset_manager.asset_registry_.asset_registry_mutex); - asset_manager.asset_registry_.assets_[asset_handle] = context.asset; + auto publish_loaded_asset = [asset_handle, context, promise]() { + context.file->asset_ = context.asset; + { + auto& asset_manager = GetInstance(); + std::lock_guard lock(asset_manager.asset_registry_.asset_registry_mutex); + asset_manager.asset_registry_.assets_[asset_handle] = context.asset; + } + promise->set_value(context.asset); + UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Loaded, "Asset loaded."); + FinishAssetLoadingImpl(asset_handle); + }; + if (context.asset->HasPendingGpuWork()) { + UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::GpuPending, "Waiting for GPU finalization."); + JobOptions gpu_ready_options; + gpu_ready_options.executor = JobExecutorType::Background; + gpu_ready_options.affinity = JobThreadAffinity::Background; + gpu_ready_options.debug_name = "AssetManager::WaitForAssetGpuReady"; + const auto gpu_ready_handle = Jobs::Run( + context.asset->GetPendingGpuWorkHandles(), gpu_ready_options, [asset_handle, context, promise]() { + try { + context.asset->WaitForPendingGpuWork(); + ScheduleMainThreadAssetTaskImpl([asset_handle, context, promise]() { + context.file->asset_ = context.asset; + { + auto& asset_manager = GetInstance(); + std::lock_guard lock(asset_manager.asset_registry_.asset_registry_mutex); + asset_manager.asset_registry_.assets_[asset_handle] = context.asset; + } + promise->set_value(context.asset); + UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Loaded, "Asset loaded."); + FinishAssetLoadingImpl(asset_handle); + }); + } catch (const std::exception& e) { + promise->set_exception(std::current_exception()); + UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, e.what()); + FinishAssetLoadingImpl(asset_handle); + } catch (...) { + promise->set_exception(std::current_exception()); + UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, + "Unknown asset GPU finalization failure."); + FinishAssetLoadingImpl(asset_handle); + } + }); + if (!gpu_ready_handle.Valid()) { + throw std::runtime_error("Failed to schedule asset GPU readiness wait."); + } + Jobs::Execute(gpu_ready_handle); + return; } - promise->set_value(context.asset); - UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Loaded, "Asset loaded."); + publish_loaded_asset(); } catch (const std::exception& e) { promise->set_exception(std::current_exception()); UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, e.what()); + FinishAssetLoadingImpl(asset_handle); } catch (...) { promise->set_exception(std::current_exception()); UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, "Unknown asset finalization failure."); + FinishAssetLoadingImpl(asset_handle); } - FinishAssetLoadingImpl(asset_handle); }); return; } diff --git a/EvoEngine_SDK/src/GeometryStorage.cpp b/EvoEngine_SDK/src/GeometryStorage.cpp index 7df7a94c..55a6b97b 100644 --- a/EvoEngine_SDK/src/GeometryStorage.cpp +++ b/EvoEngine_SDK/src/GeometryStorage.cpp @@ -1,34 +1,172 @@ #include "GeometryStorage.hpp" #include "Application.hpp" +#include "Jobs.hpp" #include "RenderLayer.hpp" #include "meshoptimizer.h" using namespace evo_engine; -void GeometryStorage::UploadData() { - if (require_mesh_data_device_update_) { - vertex_buffer_->UploadVector(vertex_data_chunks_); - meshlet_buffer_->UploadVector(meshlets_); - triangle_buffer_->UploadVector(triangles_); - require_mesh_data_device_update_ = false; +namespace { +template +GpuWorkHandle UploadVectorAsync(const std::shared_ptr& buffer, const std::vector& data) { + if (!buffer || data.empty()) { + return {}; + } + return buffer->UploadDataAsync(data.size() * sizeof(T), data.data()); +} +} // namespace - version_++; +bool GeometryStorage::GeometryUploadSnapshot::Active() const { + return mesh_dirty || mesh_upload_active || skinned_mesh_dirty || skinned_mesh_upload_active || strand_dirty || + strand_upload_active; +} + +void GeometryStorage::CaptureRangeCommits(const std::vector>& descriptors, + std::vector& commits) { + commits.clear(); + commits.reserve(descriptors.size()); + for (const auto& descriptor : descriptors) { + if (!descriptor) { + continue; + } + auto& commit = commits.emplace_back(); + commit.descriptor = descriptor; + commit.offset = descriptor->offset; + commit.range = descriptor->range; + commit.index_count = descriptor->index_count; } - if (require_skinned_mesh_data_device_update_) { - skinned_vertex_buffer_->UploadVector(skinned_vertex_data_chunks_); - skinned_meshlet_buffer_->UploadVector(skinned_meshlets_); - skinned_triangle_buffer_->UploadVector(skinned_triangles_); - require_skinned_mesh_data_device_update_ = false; +} - version_++; +void GeometryStorage::ApplyRangeCommits(const std::vector& commits) { + for (const auto& commit : commits) { + if (!commit.descriptor) { + continue; + } + commit.descriptor->prev_frame_offset = commit.offset; + commit.descriptor->prev_frame_range = commit.range; + commit.descriptor->prev_frame_index_count = commit.index_count; } - if (require_strand_mesh_data_device_update_) { - strand_point_buffer_->UploadVector(strand_point_data_chunks_); - strand_meshlet_buffer_->UploadVector(strand_meshlets_); - segment_buffer_->UploadVector(segments_); - require_strand_mesh_data_device_update_ = false; +} + +bool GeometryStorage::HasValidUploadHandle(const PendingGeometryUpload& upload) { + for (const auto& handle : upload.handles) { + if (handle.Valid()) { + return true; + } + } + return false; +} + +bool GeometryStorage::IsPendingUploadCompleted(const PendingGeometryUpload& upload) { + for (const auto& handle : upload.handles) { + if (handle.Valid() && !Jobs::IsCompleted(handle)) { + return false; + } + } + return true; +} - version_++; +void GeometryStorage::WaitPendingUpload(PendingGeometryUpload& upload) { + auto* gpu_service = Platform::TryGetGpuService(); + for (const auto& handle : upload.handles) { + if (!handle.Valid()) { + continue; + } + if (gpu_service) { + gpu_service->Wait(handle); + } else { + Jobs::Wait(handle); + } } +} + +void GeometryStorage::ClearPendingUpload(PendingGeometryUpload& upload) { + upload.active = false; + upload.handles.clear(); + upload.meshlet_commits.clear(); + upload.index_commits.clear(); +} + +bool GeometryStorage::CompletePendingUpload(PendingGeometryUpload& upload) { + if (!upload.active || !IsPendingUploadCompleted(upload)) { + return false; + } + WaitPendingUpload(upload); + ApplyRangeCommits(upload.meshlet_commits); + ApplyRangeCommits(upload.index_commits); + ClearPendingUpload(upload); + version_++; + return true; +} + +void GeometryStorage::CompletePendingUploads() { + CompletePendingUpload(pending_mesh_upload_); + CompletePendingUpload(pending_skinned_mesh_upload_); + CompletePendingUpload(pending_strand_upload_); +} + +void GeometryStorage::SchedulePendingUploads() { + if (require_mesh_data_device_update_ && !pending_mesh_upload_.active) { + pending_mesh_upload_.handles = { + UploadVectorAsync(vertex_buffer_, vertex_data_chunks_), + UploadVectorAsync(meshlet_buffer_, meshlets_), + UploadVectorAsync(triangle_buffer_, triangles_), + }; + CaptureRangeCommits(meshlet_range_descriptor_, pending_mesh_upload_.meshlet_commits); + CaptureRangeCommits(triangle_range_descriptor_, pending_mesh_upload_.index_commits); + require_mesh_data_device_update_ = false; + if (HasValidUploadHandle(pending_mesh_upload_)) { + pending_mesh_upload_.active = true; + } else { + ApplyRangeCommits(pending_mesh_upload_.meshlet_commits); + ApplyRangeCommits(pending_mesh_upload_.index_commits); + ClearPendingUpload(pending_mesh_upload_); + version_++; + } + } + + if (require_skinned_mesh_data_device_update_ && !pending_skinned_mesh_upload_.active) { + pending_skinned_mesh_upload_.handles = { + UploadVectorAsync(skinned_vertex_buffer_, skinned_vertex_data_chunks_), + UploadVectorAsync(skinned_meshlet_buffer_, skinned_meshlets_), + UploadVectorAsync(skinned_triangle_buffer_, skinned_triangles_), + }; + CaptureRangeCommits(skinned_meshlet_range_descriptor_, pending_skinned_mesh_upload_.meshlet_commits); + CaptureRangeCommits(skinned_triangle_range_descriptor_, pending_skinned_mesh_upload_.index_commits); + require_skinned_mesh_data_device_update_ = false; + if (HasValidUploadHandle(pending_skinned_mesh_upload_)) { + pending_skinned_mesh_upload_.active = true; + } else { + ApplyRangeCommits(pending_skinned_mesh_upload_.meshlet_commits); + ApplyRangeCommits(pending_skinned_mesh_upload_.index_commits); + ClearPendingUpload(pending_skinned_mesh_upload_); + version_++; + } + } + + if (require_strand_mesh_data_device_update_ && !pending_strand_upload_.active) { + pending_strand_upload_.handles = { + UploadVectorAsync(strand_point_buffer_, strand_point_data_chunks_), + UploadVectorAsync(strand_meshlet_buffer_, strand_meshlets_), + UploadVectorAsync(segment_buffer_, segments_), + }; + CaptureRangeCommits(strand_meshlet_range_descriptor_, pending_strand_upload_.meshlet_commits); + CaptureRangeCommits(segment_range_descriptor_, pending_strand_upload_.index_commits); + require_strand_mesh_data_device_update_ = false; + if (HasValidUploadHandle(pending_strand_upload_)) { + pending_strand_upload_.active = true; + } else { + ApplyRangeCommits(pending_strand_upload_.meshlet_commits); + ApplyRangeCommits(pending_strand_upload_.index_commits); + ClearPendingUpload(pending_strand_upload_); + version_++; + } + } +} + +void GeometryStorage::UploadData() { + CompletePendingUploads(); + SchedulePendingUploads(); + CompletePendingUploads(); for (int index = 0; index < particle_info_list_data_list_.size(); index++) { if (auto& particle_info_list_data = particle_info_list_data_list_.at(index); @@ -54,22 +192,6 @@ void GeometryStorage::UploadData() { const auto& particle_info_list_data = particle_info_list_data_list_.at(index); particle_info_list_data.range_descriptor->offset = index; } - const auto& storage = GetInstance(); - for (const auto& triangle_range : storage.triangle_range_descriptor_) { - triangle_range->prev_frame_index_count = triangle_range->index_count; - triangle_range->prev_frame_offset = triangle_range->offset; - } - - for (const auto& triangle_range : storage.skinned_triangle_range_descriptor_) { - triangle_range->prev_frame_index_count = triangle_range->index_count; - triangle_range->prev_frame_offset = triangle_range->offset; - triangle_range->prev_frame_offset = triangle_range->offset; - } - - for (const auto& triangle_range : storage.segment_range_descriptor_) { - triangle_range->prev_frame_index_count = triangle_range->index_count; - triangle_range->prev_frame_offset = triangle_range->offset; - } } void GeometryStorage::DeviceSync() { @@ -136,6 +258,43 @@ uint32_t GeometryStorage::GetVersion() { return GetInstance().version_; } +GeometryStorage::GeometryUploadSnapshot GeometryStorage::GetUploadSnapshot() { + const auto& storage = GetInstance(); + GeometryUploadSnapshot snapshot; + snapshot.mesh_dirty = storage.require_mesh_data_device_update_; + snapshot.mesh_upload_active = storage.pending_mesh_upload_.active; + snapshot.mesh_upload_completed = IsPendingUploadCompleted(storage.pending_mesh_upload_); + snapshot.mesh_upload_handles = storage.pending_mesh_upload_.handles.size(); + + snapshot.skinned_mesh_dirty = storage.require_skinned_mesh_data_device_update_; + snapshot.skinned_mesh_upload_active = storage.pending_skinned_mesh_upload_.active; + snapshot.skinned_mesh_upload_completed = IsPendingUploadCompleted(storage.pending_skinned_mesh_upload_); + snapshot.skinned_mesh_upload_handles = storage.pending_skinned_mesh_upload_.handles.size(); + + snapshot.strand_dirty = storage.require_strand_mesh_data_device_update_; + snapshot.strand_upload_active = storage.pending_strand_upload_.active; + snapshot.strand_upload_completed = IsPendingUploadCompleted(storage.pending_strand_upload_); + snapshot.strand_upload_handles = storage.pending_strand_upload_.handles.size(); + return snapshot; +} + +bool GeometryStorage::HasPendingUploads() { + return GetUploadSnapshot().Active(); +} + +void GeometryStorage::WaitForPendingUploads() { + if (!Platform::Initialized()) { + return; + } + auto& storage = GetInstance(); + storage.CompletePendingUploads(); + storage.SchedulePendingUploads(); + WaitPendingUpload(storage.pending_mesh_upload_); + WaitPendingUpload(storage.pending_skinned_mesh_upload_); + WaitPendingUpload(storage.pending_strand_upload_); + storage.CompletePendingUploads(); +} + const std::shared_ptr& GeometryStorage::GetTriangleBuffer() { const auto& storage = GetInstance(); return storage.triangle_buffer_; @@ -215,6 +374,8 @@ void GeometryStorage::AllocateMesh(const Handle& handle, std::vector& ve throw std::runtime_error("Empty vertices or triangles!"); } auto& storage = GetInstance(); + WaitPendingUpload(storage.pending_mesh_upload_); + storage.CompletePendingUpload(storage.pending_mesh_upload_); // const auto meshletRange = std::make_shared(); target_meshlet_range->handle_ = handle; @@ -299,6 +460,8 @@ void GeometryStorage::AllocateSkinnedMesh(const Handle& handle, const std::vecto throw std::runtime_error("Empty skinned vertices or skinned_triangles!"); } auto& storage = GetInstance(); + WaitPendingUpload(storage.pending_skinned_mesh_upload_); + storage.CompletePendingUpload(storage.pending_skinned_mesh_upload_); target_skinned_meshlet_range->handle_ = handle; target_skinned_meshlet_range->offset = storage.skinned_meshlets_.size(); @@ -370,6 +533,8 @@ void GeometryStorage::AllocateStrands(const Handle& handle, const std::vectorhandle_ = handle; @@ -496,6 +661,8 @@ void GeometryStorage::FreeMesh(const Handle& handle) { if (!storage.initialized_) { return; } + WaitPendingUpload(storage.pending_mesh_upload_); + storage.CompletePendingUpload(storage.pending_mesh_upload_); uint32_t meshlet_range_descriptor_index = UINT_MAX; for (int i = 0; i < storage.meshlet_range_descriptor_.size(); i++) { if (storage.meshlet_range_descriptor_[i]->handle_ == handle) { @@ -556,6 +723,8 @@ void GeometryStorage::FreeSkinnedMesh(const Handle& handle) { if (!storage.initialized_) { return; } + WaitPendingUpload(storage.pending_skinned_mesh_upload_); + storage.CompletePendingUpload(storage.pending_skinned_mesh_upload_); uint32_t skinned_meshlet_range_descriptor_index = UINT_MAX; for (int i = 0; i < storage.skinned_meshlet_range_descriptor_.size(); i++) { if (storage.skinned_meshlet_range_descriptor_[i]->handle_ == handle) { @@ -623,6 +792,8 @@ void GeometryStorage::FreeStrands(const Handle& handle) { if (!storage.initialized_) { return; } + WaitPendingUpload(storage.pending_strand_upload_); + storage.CompletePendingUpload(storage.pending_strand_upload_); uint32_t strand_meshlet_range_descriptor_index = UINT_MAX; for (int i = 0; i < storage.strand_meshlet_range_descriptor_.size(); i++) { if (storage.strand_meshlet_range_descriptor_[i]->handle_ == handle) { @@ -757,6 +928,16 @@ const StrandMeshlet& GeometryStorage::PeekStrandMeshlet(const uint32_t strand_me void GeometryStorage::OnDestroy() { auto& storage = GetInstance(); + WaitPendingUpload(storage.pending_mesh_upload_); + storage.CompletePendingUpload(storage.pending_mesh_upload_); + WaitPendingUpload(storage.pending_skinned_mesh_upload_); + storage.CompletePendingUpload(storage.pending_skinned_mesh_upload_); + WaitPendingUpload(storage.pending_strand_upload_); + storage.CompletePendingUpload(storage.pending_strand_upload_); + ClearPendingUpload(storage.pending_mesh_upload_); + ClearPendingUpload(storage.pending_skinned_mesh_upload_); + ClearPendingUpload(storage.pending_strand_upload_); + storage.vertex_data_chunks_.clear(); storage.meshlets_.clear(); storage.meshlet_range_descriptor_.clear(); diff --git a/EvoEngine_SDK/src/GraphicsResources.cpp b/EvoEngine_SDK/src/GraphicsResources.cpp index e372010e..a3b98fdb 100644 --- a/EvoEngine_SDK/src/GraphicsResources.cpp +++ b/EvoEngine_SDK/src/GraphicsResources.cpp @@ -7,6 +7,8 @@ #include "RenderInstanceStorage.hpp" #include "Utilities.hpp" +#include + using namespace evo_engine; Fence::Fence(const VkFenceCreateInfo& vk_fence_create_info) { @@ -953,11 +955,38 @@ void Buffer::WaitForPendingGpuWork() const { pending_work.swap(gpu_state_->pending_gpu_work); } auto* gpu_service = Platform::TryGetGpuService(); + if (!Platform::Initialized() || !gpu_service) { + return; + } + const auto lifecycle_state = gpu_service->GetLifecycleState(); + if (lifecycle_state == GpuService::LifecycleState::Uninitialized || + lifecycle_state == GpuService::LifecycleState::Stopped) { + return; + } + + const bool on_gpu_thread = gpu_service && gpu_service->IsGpuThread(); + std::vector deferred_work; for (const auto& handle : pending_work) { - if (gpu_service) { - gpu_service->Wait(handle); - } else { - Jobs::Wait(handle); + if (!handle.Valid() || Jobs::IsCompleted(handle)) { + continue; + } + if (on_gpu_thread) { + deferred_work.emplace_back(handle); + continue; + } + gpu_service->Wait(handle); + } + + if (!deferred_work.empty()) { + deferred_work.erase(std::remove_if(deferred_work.begin(), deferred_work.end(), + [](const GpuWorkHandle& handle) { + return !handle.Valid() || Jobs::IsCompleted(handle); + }), + deferred_work.end()); + if (!deferred_work.empty()) { + std::lock_guard lock(gpu_state_->pending_gpu_work_mutex); + gpu_state_->pending_gpu_work.insert(gpu_state_->pending_gpu_work.end(), deferred_work.begin(), + deferred_work.end()); } } } diff --git a/EvoEngine_SDK/src/IAsset.cpp b/EvoEngine_SDK/src/IAsset.cpp index f6a99f47..1bb71b81 100644 --- a/EvoEngine_SDK/src/IAsset.cpp +++ b/EvoEngine_SDK/src/IAsset.cpp @@ -114,6 +114,57 @@ bool IAsset::ApplyStagedPayloadInternal(const std::filesystem::path &, return true; } +void IAsset::TrackPendingGpuWork(const JobHandle &handle) { + if (!handle.Valid()) { + return; + } + if (!pending_gpu_work_state_) { + pending_gpu_work_state_ = std::make_shared(); + } + std::lock_guard lock(pending_gpu_work_state_->mutex); + pending_gpu_work_state_->handles.emplace_back(handle); +} + +bool IAsset::HasPendingGpuWork() const { + if (!pending_gpu_work_state_) { + return false; + } + std::lock_guard lock(pending_gpu_work_state_->mutex); + for (const auto &handle : pending_gpu_work_state_->handles) { + if (handle.Valid()) { + return true; + } + } + return false; +} + +std::vector IAsset::GetPendingGpuWorkHandles() const { + if (!pending_gpu_work_state_) { + return {}; + } + std::lock_guard lock(pending_gpu_work_state_->mutex); + std::vector handles; + handles.reserve(pending_gpu_work_state_->handles.size()); + for (const auto &handle : pending_gpu_work_state_->handles) { + if (handle.Valid()) { + handles.emplace_back(handle); + } + } + return handles; +} + +void IAsset::WaitForPendingGpuWork() const { + const auto handles = GetPendingGpuWorkHandles(); + for (const auto &handle : handles) { + Jobs::Wait(handle); + } + if (!pending_gpu_work_state_) { + return; + } + std::lock_guard lock(pending_gpu_work_state_->mutex); + pending_gpu_work_state_->handles.clear(); +} + void IAsset::OnCreate() { } diff --git a/EvoEngine_SDK/src/ProjectManager.cpp b/EvoEngine_SDK/src/ProjectManager.cpp index 12f983a5..f0793558 100644 --- a/EvoEngine_SDK/src/ProjectManager.cpp +++ b/EvoEngine_SDK/src/ProjectManager.cpp @@ -775,8 +775,8 @@ void ProjectManager::OnInspect(const std::shared_ptr& editor_layer) ImGui::Text("Progress: "); const auto completed_asset_count = asset_load_snapshot.completed + asset_load_snapshot.failed + asset_load_snapshot.cancelled; - const auto active_asset_count = - asset_load_snapshot.queued + asset_load_snapshot.loading_cpu + asset_load_snapshot.waiting_for_finalize; + const auto active_asset_count = asset_load_snapshot.queued + asset_load_snapshot.loading_cpu + + asset_load_snapshot.waiting_for_finalize + asset_load_snapshot.gpu_pending; auto total_asset_count = asset_load_snapshot.total; total_asset_count = std::max(total_asset_count, completed_asset_count + active_asset_count); total_asset_count = std::max(total_asset_count, project_manager.pending_asset_size); diff --git a/EvoEngine_SDK/src/RenderInstanceStorage.cpp b/EvoEngine_SDK/src/RenderInstanceStorage.cpp index d9c176e2..3be75c88 100644 --- a/EvoEngine_SDK/src/RenderInstanceStorage.cpp +++ b/EvoEngine_SDK/src/RenderInstanceStorage.cpp @@ -129,9 +129,9 @@ void RenderInstanceStorage::MeshRenderInstance::Apply(InstanceInfoBlock& instanc instance_info_block.model = model; instance_info_block.material_index = material_index; instance_info_block.info_index = entity_selected ? 1 : 0; - instance_info_block.triangle_offset = mesh->triangle_range_->offset; - instance_info_block.meshlet_index_offset = mesh->meshlet_range_->offset; - instance_info_block.meshlet_size = mesh->meshlet_range_->range; + instance_info_block.triangle_offset = mesh->triangle_range_->prev_frame_offset; + instance_info_block.meshlet_index_offset = mesh->meshlet_range_->prev_frame_offset; + instance_info_block.meshlet_size = mesh->meshlet_range_->prev_frame_range; instance_info_block.entity_index = owner.GetIndex(); instance_info_block.renderer_handle = renderer_handle; } @@ -146,12 +146,12 @@ uint32_t RenderInstanceStorage::MeshRenderInstance::Render( if (Platform::MeshShaderEnabled()) { graphics_pipeline->states.ApplyAllStates(vk_command_buffer); const uint32_t count = - (mesh->meshlet_range_->range + task_work_group_invocations - 1) / task_work_group_invocations; + (mesh->meshlet_range_->prev_frame_range + task_work_group_invocations - 1) / task_work_group_invocations; graphics_pipeline->DrawMeshTasks(vk_command_buffer, count); } else { mesh->DrawIndexed(vk_command_buffer, graphics_pipeline->states, 1); } - return mesh->GetTriangleAmount(); + return mesh->triangle_range_->prev_frame_index_count; } bool RenderInstanceStorage::SkinnedMeshRenderInstance::operator!=(const SkinnedMeshRenderInstance& other) const { @@ -192,9 +192,9 @@ void RenderInstanceStorage::SkinnedMeshRenderInstance::Apply(InstanceInfoBlock& instance_info_block.model = model; instance_info_block.material_index = material_index; instance_info_block.info_index = entity_selected ? 1 : 0; - instance_info_block.triangle_offset = skinned_mesh->skinned_triangle_range_->offset; - instance_info_block.meshlet_index_offset = skinned_mesh->skinned_meshlet_range_->offset; - instance_info_block.meshlet_size = skinned_mesh->skinned_meshlet_range_->range; + instance_info_block.triangle_offset = skinned_mesh->skinned_triangle_range_->prev_frame_offset; + instance_info_block.meshlet_index_offset = skinned_mesh->skinned_meshlet_range_->prev_frame_offset; + instance_info_block.meshlet_size = skinned_mesh->skinned_meshlet_range_->prev_frame_range; instance_info_block.entity_index = owner.GetIndex(); instance_info_block.renderer_handle = renderer_handle; } @@ -205,7 +205,7 @@ uint32_t RenderInstanceStorage::SkinnedMeshRenderInstance::Render( graphics_pipeline->BindDescriptorSet(vk_command_buffer, 1, bone_matrices->GetDescriptorSet()->GetVkDescriptorSet()); graphics_pipeline->PushConstant(vk_command_buffer, 0, render_instance_push_constant); skinned_mesh->DrawIndexed(vk_command_buffer, graphics_pipeline->states, 1); - return skinned_mesh->GetTriangleAmount(); + return skinned_mesh->skinned_triangle_range_->prev_frame_index_count; } bool RenderInstanceStorage::InstancedRenderInstance::operator!=(const InstancedRenderInstance& other) const { @@ -246,9 +246,9 @@ void RenderInstanceStorage::InstancedRenderInstance::Apply(InstanceInfoBlock& in instance_info_block.model = model; instance_info_block.material_index = material_index; instance_info_block.info_index = entity_selected ? 1 : 0; - instance_info_block.triangle_offset = mesh->triangle_range_->offset; - instance_info_block.meshlet_index_offset = mesh->meshlet_range_->offset; - instance_info_block.meshlet_size = mesh->meshlet_range_->range; + instance_info_block.triangle_offset = mesh->triangle_range_->prev_frame_offset; + instance_info_block.meshlet_index_offset = mesh->meshlet_range_->prev_frame_offset; + instance_info_block.meshlet_size = mesh->meshlet_range_->prev_frame_range; instance_info_block.entity_index = owner.GetIndex(); instance_info_block.renderer_handle = renderer_handle; } @@ -259,7 +259,7 @@ uint32_t RenderInstanceStorage::InstancedRenderInstance::Render( graphics_pipeline->BindDescriptorSet(vk_command_buffer, 1, particle_infos->GetDescriptorSet()->GetVkDescriptorSet()); graphics_pipeline->PushConstant(vk_command_buffer, 0, render_instance_push_constant); mesh->DrawIndexed(vk_command_buffer, graphics_pipeline->states, particle_infos->PeekParticleInfoList().size()); - return mesh->UnsafeGetTriangles().size() * particle_infos->PeekParticleInfoList().size(); + return mesh->triangle_range_->prev_frame_index_count * particle_infos->PeekParticleInfoList().size(); } bool RenderInstanceStorage::StrandsRenderInstance::operator!=(const StrandsRenderInstance& other) const { @@ -298,9 +298,9 @@ void RenderInstanceStorage::StrandsRenderInstance::Apply(InstanceInfoBlock& inst instance_info_block.model = model; instance_info_block.material_index = material_index; instance_info_block.info_index = entity_selected ? 1 : 0; - instance_info_block.triangle_offset = strands->segment_range_->offset; - instance_info_block.meshlet_index_offset = strands->strand_meshlet_range_->offset; - instance_info_block.meshlet_size = strands->strand_meshlet_range_->range; + instance_info_block.triangle_offset = strands->segment_range_->prev_frame_offset; + instance_info_block.meshlet_index_offset = strands->strand_meshlet_range_->prev_frame_offset; + instance_info_block.meshlet_size = strands->strand_meshlet_range_->prev_frame_range; instance_info_block.entity_index = owner.GetIndex(); instance_info_block.renderer_handle = renderer_handle; } @@ -310,7 +310,7 @@ uint32_t RenderInstanceStorage::StrandsRenderInstance::Render( const std::shared_ptr& graphics_pipeline) const { graphics_pipeline->PushConstant(vk_command_buffer, 0, render_instance_push_constant); strands->DrawIndexed(vk_command_buffer, graphics_pipeline->states, 1); - return strands->GetSegmentAmount(); + return strands->segment_range_->prev_frame_index_count; } bool RenderInstanceStorage::ExternalRenderInstanceCollection::operator!=( @@ -1340,6 +1340,8 @@ bool RenderInstanceStorage::RegisterMeshDrawCommand(const std::shared_ptr& return false; if (mesh->UnsafeGetVertices().empty() || mesh->UnsafeGetTriangles().empty()) return false; + if (mesh->triangle_range_->prev_frame_index_count == 0 || mesh->meshlet_range_->prev_frame_range == 0) + return false; MaterialInfoBlock material_info_block; material_info_block.Apply(material); const auto render_instance = std::make_shared(); @@ -1366,18 +1368,21 @@ bool RenderInstanceStorage::RegisterMeshDrawCommand(const std::shared_ptr& } auto& new_mesh_task = mesh_draw_mesh_tasks_indirect_commands.emplace_back(); - new_mesh_task.groupCountX = 1; + const uint32_t task_work_group_invocations = + Platform::GetSelectedPhysicalDevice()->mesh_shader_properties_ext.maxPreferredTaskWorkGroupInvocations; + new_mesh_task.groupCountX = + (mesh->meshlet_range_->prev_frame_range + task_work_group_invocations - 1) / task_work_group_invocations; new_mesh_task.groupCountY = 1; new_mesh_task.groupCountZ = 1; auto& new_draw_task = mesh_draw_indexed_indirect_commands.emplace_back(); new_draw_task.instanceCount = 1; - new_draw_task.firstIndex = mesh->triangle_range_->offset * 3; - new_draw_task.indexCount = static_cast(mesh->triangles_.size() * 3); + new_draw_task.firstIndex = mesh->triangle_range_->prev_frame_offset * 3; + new_draw_task.indexCount = mesh->triangle_range_->prev_frame_index_count * 3; new_draw_task.vertexOffset = 0; new_draw_task.firstInstance = 0; - total_mesh_triangles += mesh->triangles_.size(); + total_mesh_triangles += mesh->triangle_range_->prev_frame_index_count; return true; } @@ -1389,6 +1394,8 @@ bool RenderInstanceStorage::RegisterMeshDrawInstancedCommand( return false; if (mesh->UnsafeGetVertices().empty() || mesh->UnsafeGetTriangles().empty()) return false; + if (mesh->triangle_range_->prev_frame_index_count == 0 || mesh->meshlet_range_->prev_frame_range == 0) + return false; MaterialInfoBlock material_info_block; material_info_block.Apply(material); const auto render_instance = std::make_shared(); @@ -1416,7 +1423,8 @@ bool RenderInstanceStorage::RegisterMeshDrawInstancedCommand( deferred_instanced_render_instances->Register(render_instance); } - total_mesh_triangles += mesh->triangles_.size() * particle_info_list->PeekParticleInfoList().size(); + total_mesh_triangles += + mesh->triangle_range_->prev_frame_index_count * particle_info_list->PeekParticleInfoList().size(); return true; } @@ -1493,6 +1501,8 @@ bool RenderInstanceStorage::RegisterEntity(const std::shared_ptr& target_ if (!strands_renderer->IsEnabled() || !material || !strands || !strands->strand_meshlet_range_ || !strands->segment_range_) return false; + if (strands->segment_range_->prev_frame_index_count == 0 || strands->strand_meshlet_range_->prev_frame_range == 0) + return false; auto gt = target_scene->GetDataComponent(owner); auto ltw = gt.value; auto mesh_bound = strands->bound_; @@ -1531,7 +1541,7 @@ bool RenderInstanceStorage::RegisterEntity(const std::shared_ptr& target_ deferred_strands_render_instances->Register(render_instance); } - total_strands_segments += strands->segments_.size(); + total_strands_segments += strands->segment_range_->prev_frame_index_count; return true; } bool RenderInstanceStorage::RegisterEntity(const std::shared_ptr& target_scene, const Entity& owner, @@ -1543,6 +1553,8 @@ bool RenderInstanceStorage::RegisterEntity(const std::shared_ptr& target_ return false; if (mesh->UnsafeGetVertices().empty() || mesh->UnsafeGetTriangles().empty()) return false; + if (mesh->triangle_range_->prev_frame_index_count == 0 || mesh->meshlet_range_->prev_frame_range == 0) + return false; auto gt = target_scene->GetDataComponent(owner); auto ltw = gt.value; @@ -1585,19 +1597,19 @@ bool RenderInstanceStorage::RegisterEntity(const std::shared_ptr& target_ Platform::GetSelectedPhysicalDevice()->mesh_shader_properties_ext.maxPreferredTaskWorkGroupInvocations; auto& new_mesh_task = mesh_draw_mesh_tasks_indirect_commands.emplace_back(); const uint32_t count = - (mesh->meshlet_range_->range + task_work_group_invocations - 1) / task_work_group_invocations; + (mesh->meshlet_range_->prev_frame_range + task_work_group_invocations - 1) / task_work_group_invocations; new_mesh_task.groupCountX = count; new_mesh_task.groupCountY = 1; new_mesh_task.groupCountZ = 1; auto& new_draw_task = mesh_draw_indexed_indirect_commands.emplace_back(); new_draw_task.instanceCount = 1; - new_draw_task.firstIndex = mesh->triangle_range_->offset * 3; - new_draw_task.indexCount = static_cast(mesh->triangles_.size() * 3); + new_draw_task.firstIndex = mesh->triangle_range_->prev_frame_offset * 3; + new_draw_task.indexCount = mesh->triangle_range_->prev_frame_index_count * 3; new_draw_task.vertexOffset = 0; new_draw_task.firstInstance = 0; } - total_mesh_triangles += mesh->triangles_.size(); + total_mesh_triangles += mesh->triangle_range_->prev_frame_index_count; return true; } @@ -1611,6 +1623,9 @@ bool RenderInstanceStorage::RegisterEntity(const std::shared_ptr& target_ return false; if (skinned_mesh->skinned_vertices_.empty() || skinned_mesh->skinned_triangles_.empty()) return false; + if (skinned_mesh->skinned_triangle_range_->prev_frame_index_count == 0 || + skinned_mesh->skinned_meshlet_range_->prev_frame_range == 0) + return false; GlobalTransform gt; if (auto animator = skinned_mesh_renderer->animator.Get(); !animator) { return false; @@ -1656,7 +1671,7 @@ bool RenderInstanceStorage::RegisterEntity(const std::shared_ptr& target_ deferred_skinned_render_instances->Register(render_instance); } - total_skinned_mesh_triangles += skinned_mesh->skinned_triangles_.size(); + total_skinned_mesh_triangles += skinned_mesh->skinned_triangle_range_->prev_frame_index_count; return true; } @@ -1671,6 +1686,8 @@ bool RenderInstanceStorage::RegisterEntity(const std::shared_ptr& target_ return false; if (particle_info_list->PeekParticleInfoList().empty()) return false; + if (mesh->triangle_range_->prev_frame_index_count == 0 || mesh->meshlet_range_->prev_frame_range == 0) + return false; auto gt = target_scene->GetDataComponent(owner); auto ltw = gt.value; auto mesh_bound = mesh->GetBound(); @@ -1712,7 +1729,8 @@ bool RenderInstanceStorage::RegisterEntity(const std::shared_ptr& target_ deferred_instanced_render_instances->Register(render_instance); } - total_instanced_mesh_triangles += mesh->triangles_.size() * particle_info_list->PeekParticleInfoList().size(); + total_instanced_mesh_triangles += + mesh->triangle_range_->prev_frame_index_count * particle_info_list->PeekParticleInfoList().size(); return true; } diff --git a/EvoEngine_SDK/src/RenderLayer.cpp b/EvoEngine_SDK/src/RenderLayer.cpp index 53892b71..0f7604c7 100644 --- a/EvoEngine_SDK/src/RenderLayer.cpp +++ b/EvoEngine_SDK/src/RenderLayer.cpp @@ -3,6 +3,7 @@ #include "AssetManager.hpp" #include "EditorLayer.hpp" #include "GeometryStorage.hpp" +#include "GpuService.hpp" #include "GraphicsPipeline.hpp" #include "Jobs.hpp" #include "LodGroup.hpp" @@ -2220,6 +2221,19 @@ void RenderLayer::PreUpdate() { scene->SetBound({}); } +void RenderLayer::OnDestroy() { + if (!Platform::Initialized()) { + return; + } + GeometryStorage::WaitForPendingUploads(); + TextureStorage::DeviceSync(); + if (const auto gpu_service = Platform::TryGetGpuService(); + gpu_service && gpu_service->GetLifecycleState() == GpuService::LifecycleState::Running) { + gpu_service->WaitIdle(); + } + Platform::WaitForDeviceIdle(); +} + uint32_t RenderLayer::DrawMesh(const std::shared_ptr& mesh, const std::shared_ptr& material, const GlobalTransform& global_transform, const bool cast_shadow) const { const auto current_frame_index = Platform::GetCurrentFrameIndex(); diff --git a/EvoEngine_SDK/src/Resources.cpp b/EvoEngine_SDK/src/Resources.cpp index ddd31b40..6694e6fe 100644 --- a/EvoEngine_SDK/src/Resources.cpp +++ b/EvoEngine_SDK/src/Resources.cpp @@ -3,6 +3,7 @@ #include "Cubemap.hpp" #include "EditorLayer.hpp" #include "GeometryStorage.hpp" +#include "GpuService.hpp" #include "ProjectManager.hpp" #include "RenderLayer.hpp" #include "Shader.hpp" @@ -154,6 +155,7 @@ void Resources::Initialize() { resources.LoadPrimitives(); GeometryStorage::DeviceSync(); + GeometryStorage::WaitForPendingUploads(); TextureStorage::DeviceSync(); resources.missing_texture_ = CreateResource(); resources.missing_texture_->LoadInternal(std::filesystem::path("./DefaultResources") / @@ -244,6 +246,16 @@ bool Resources::IsResource(const AssetRef& target) { void Resources::OnDestroy() { auto& resources = GetInstance(); + if (Platform::Initialized()) { + GeometryStorage::WaitForPendingUploads(); + TextureStorage::DeviceSync(); + if (const auto gpu_service = Platform::TryGetGpuService(); + gpu_service && gpu_service->GetLifecycleState() == GpuService::LifecycleState::Running) { + gpu_service->WaitIdle(); + } + Platform::WaitForDeviceIdle(); + } + resources.typed_resources_.clear(); resources.resources_.clear(); diff --git a/EvoEngine_SDK/src/TaskRuntime.cpp b/EvoEngine_SDK/src/TaskRuntime.cpp index 4b86b4d0..ae12a090 100644 --- a/EvoEngine_SDK/src/TaskRuntime.cpp +++ b/EvoEngine_SDK/src/TaskRuntime.cpp @@ -324,8 +324,11 @@ void TaskRuntime::Wait(const TaskHandle& handle) { } bool TaskRuntime::IsCompleted(const TaskHandle& handle) const { + if (!handle.Valid()) { + return true; + } std::lock_guard lock(task_mutex_); - return IsHandleValidLocked(handle) && tasks_[handle.index_]->state == TaskState::Completed; + return !IsHandleValidLocked(handle) || tasks_[handle.index_]->state == TaskState::Completed; } std::exception_ptr TaskRuntime::GetException(const TaskHandle& handle) const { diff --git a/EvoEngine_SDK/src/Texture2D.cpp b/EvoEngine_SDK/src/Texture2D.cpp index 2b5541b7..35a9415b 100644 --- a/EvoEngine_SDK/src/Texture2D.cpp +++ b/EvoEngine_SDK/src/Texture2D.cpp @@ -81,13 +81,18 @@ void DecodeSerializedTexture2D(const YAML::Node& in, Texture2DStagedLoadPayload& void Texture2D::SetData(const std::vector& data, const glm::uvec2& resolution, const bool local_copy) { auto& texture_storage = TextureStorage::RefTexture2DStorage(texture_storage_handle_); - texture_storage.SetData(data, resolution); + if (Platform::Initialized()) { + TrackPendingGpuWork(texture_storage.SetDataAsync(data, resolution)); + } else { + texture_storage.SetData(data, resolution); + } if (local_copy) { local_data_ = data; } } void Texture2D::DownloadData() { + WaitForPendingGpuWork(); const auto& texture_storage = PeekTexture2DStorage(); const auto resolution = GetResolution(); local_data_.resize(resolution.x * resolution.y); @@ -99,6 +104,7 @@ void Texture2D::DownloadData() { void Texture2D::UnsafeUploadDataImmediately() const { auto& texture_storage = TextureStorage::RefTexture2DStorage(texture_storage_handle_); texture_storage.UploadDataImmediately(); + WaitForPendingGpuWork(); } bool Texture2D::SaveInternal(const std::filesystem::path& path) const { if (path.extension() == ".png") { @@ -254,7 +260,7 @@ bool Texture2D::ApplyStagedPayloadInternal(const std::filesystem::path&, if (!local_data_.empty() && texture_payload->resolution.x != 0 && texture_payload->resolution.y != 0) { auto& texture_storage = TextureStorage::RefTexture2DStorage(texture_storage_handle_); - texture_storage.SetDataImmediately(local_data_, texture_payload->resolution); + TrackPendingGpuWork(texture_storage.SetDataAsync(local_data_, texture_payload->resolution)); } return true; } @@ -280,7 +286,11 @@ void Texture2D::SetResolution(const glm::uvec2& resolution, bool preserve_data) if (preserve_data && !local_data_.empty()) { const auto copy = local_data_; Resize(copy, GetResolution(), local_data_, resolution); - texture_storage.SetData(local_data_, resolution); + if (Platform::Initialized()) { + TrackPendingGpuWork(texture_storage.SetDataAsync(local_data_, resolution)); + } else { + texture_storage.SetData(local_data_, resolution); + } } else { texture_storage.Initialize(resolution); } @@ -443,6 +453,7 @@ glm::uvec2 Texture2D::GetResolution() const { void Texture2D::StoreToPng(const std::filesystem::path& path, const int resize_x, const int resize_y, const unsigned compression_level) const { + WaitForPendingGpuWork(); const auto& texture_storage = PeekTexture2DStorage(); const auto resolution = GetResolution(); @@ -505,6 +516,7 @@ void Texture2D::StoreToPng(const std::filesystem::path& path, const std::vector< } void Texture2D::StoreToTga(const std::filesystem::path& path, const int resize_x, const int resize_y) const { + WaitForPendingGpuWork(); const auto& texture_storage = PeekTexture2DStorage(); const auto resolution = GetResolution(); @@ -537,6 +549,7 @@ void Texture2D::StoreToTga(const std::filesystem::path& path, const int resize_x void Texture2D::StoreToJpg(const std::filesystem::path& path, const int resize_x, const int resize_y, const unsigned quality) const { + WaitForPendingGpuWork(); const auto& texture_storage = PeekTexture2DStorage(); const auto resolution = GetResolution(); @@ -666,6 +679,7 @@ void Texture2D::StoreToHdr(const std::filesystem::path& path, const std::vector< } void Texture2D::StoreToHdr(const std::filesystem::path& path, const int resize_x, const int resize_y) const { + WaitForPendingGpuWork(); const auto& texture_storage = PeekTexture2DStorage(); const auto resolution = GetResolution(); @@ -750,6 +764,7 @@ std::shared_ptr Texture2D::GenerateThumbnailTexture() { } void Texture2D::GetRgbaChannelData(std::vector& dst, const int resize_x, const int resize_y) const { + WaitForPendingGpuWork(); const auto& texture_storage = PeekTexture2DStorage(); const auto resolution = GetResolution(); if ((resize_x == -1 && resize_y == -1) || (resolution.x == resize_x && resolution.y == resize_y)) { @@ -771,6 +786,7 @@ void Texture2D::GetRgbaChannelData(std::vector& dst, const int resize } void Texture2D::GetRgbChannelData(std::vector& dst, int resize_x, int resize_y) const { + WaitForPendingGpuWork(); const auto& texture_storage = PeekTexture2DStorage(); const auto resolution = GetResolution(); std::vector pixels; @@ -785,6 +801,7 @@ void Texture2D::GetRgbChannelData(std::vector& dst, int resize_x, int } void Texture2D::GetRgChannelData(std::vector& dst, int resize_x, int resize_y) const { + WaitForPendingGpuWork(); const auto& texture_storage = PeekTexture2DStorage(); const auto resolution = GetResolution(); std::vector pixels; @@ -799,6 +816,7 @@ void Texture2D::GetRgChannelData(std::vector& dst, int resize_x, int } void Texture2D::GetRedChannelData(std::vector& dst, int resize_x, int resize_y) const { + WaitForPendingGpuWork(); const auto& texture_storage = PeekTexture2DStorage(); const auto resolution = GetResolution(); std::vector pixels; diff --git a/EvoEngine_SDK/src/TextureStorage.cpp b/EvoEngine_SDK/src/TextureStorage.cpp index 4ed8e1db..06c6f859 100644 --- a/EvoEngine_SDK/src/TextureStorage.cpp +++ b/EvoEngine_SDK/src/TextureStorage.cpp @@ -3,8 +3,60 @@ #include "Application.hpp" #include "EditorLayer.hpp" #include "RenderLayer.hpp" + +#include + using namespace evo_engine; +namespace { +class PendingGpuUploadCompletion { + std::shared_ptr counter_; + + public: + explicit PendingGpuUploadCompletion(std::shared_ptr counter) : counter_(std::move(counter)) { + } + + ~PendingGpuUploadCompletion() { + if (counter_) { + counter_->fetch_sub(1); + } + } + + PendingGpuUploadCompletion(const PendingGpuUploadCompletion&) = delete; + PendingGpuUploadCompletion& operator=(const PendingGpuUploadCompletion&) = delete; +}; + +std::shared_ptr> BuildTextureUploadBytes(const std::vector& data, + const glm::uvec2& resolution) { + const auto pixel_size = static_cast(resolution.x) * static_cast(resolution.y); + if (pixel_size == 0 || data.size() < pixel_size) { + return {}; + } + + switch (Platform::Constants::texture_2d) { + case VK_FORMAT_R32G32B32A32_SFLOAT: { + auto bytes = std::make_shared>(pixel_size * sizeof(glm::vec4)); + memcpy(bytes->data(), data.data(), bytes->size()); + return bytes; + } + case VK_FORMAT_R16G16B16A16_SFLOAT: { + std::vector half_size_data(4 * pixel_size); + Jobs::RunParallelFor(pixel_size, [&](const auto i) { + half_size_data[i * 4] = glm::detail::toFloat16(data[i][0]); + half_size_data[i * 4 + 1] = glm::detail::toFloat16(data[i][1]); + half_size_data[i * 4 + 2] = glm::detail::toFloat16(data[i][2]); + half_size_data[i * 4 + 3] = glm::detail::toFloat16(data[i][3]); + }); + auto bytes = std::make_shared>(half_size_data.size() * sizeof(glm::detail::hdata)); + memcpy(bytes->data(), half_size_data.data(), bytes->size()); + return bytes; + } + default: + throw std::runtime_error("Unsupported Texture2D upload format."); + } +} +} // namespace + void CubemapStorage::Initialize(uint32_t resolution, uint32_t mip_levels) { if (!Platform::Initialized()) return; @@ -141,6 +193,10 @@ std::shared_ptr CubemapStorage::GetImage() const { return image; } +bool Texture2DStorage::IsGpuUploadPending() const { + return gpu_upload_in_flight && gpu_upload_in_flight->load() != 0; +} + void Texture2DStorage::Initialize(const glm::uvec2& resolution) { if (!Platform::Initialized()) return; @@ -208,6 +264,51 @@ void Texture2DStorage::SetDataImmediately(const std::vector& data, co UploadData(data, resolution); } +GpuWorkHandle Texture2DStorage::SetDataAsync(const std::vector& data, const glm::uvec2& resolution) { + if (!Platform::Initialized() || data.empty() || resolution.x == 0 || resolution.y == 0) { + return {}; + } + Initialize(resolution); + auto upload_bytes = BuildTextureUploadBytes(data, resolution); + if (!upload_bytes || upload_bytes->empty()) { + return {}; + } + + const auto target_image = image; + const auto pending_counter = gpu_upload_in_flight; + pending_counter->fetch_add(1); + GpuWorkOptions options; + options.debug_name = "Texture2DStorage::SetDataAsync"; + auto& gpu_service = Platform::GetGpuService(); + try { + return gpu_service.EnqueueStaging(upload_bytes->size(), options, [target_image, pending_counter, upload_bytes]() { + const PendingGpuUploadCompletion pending_completion(pending_counter); + auto& gpu_service = Platform::GetGpuService(); + auto staging_buffer = gpu_service.AcquireStagingBuffer(upload_bytes->size(), false); + try { + void* mapping; + Platform::CheckVk(vmaMapMemory(Platform::GetVmaAllocator(), staging_buffer.vma_allocation, &mapping)); + memcpy(mapping, upload_bytes->data(), upload_bytes->size()); + vmaUnmapMemory(Platform::GetVmaAllocator(), staging_buffer.vma_allocation); + gpu_service.SubmitImmediate( + [target_image, staging_vk_buffer = staging_buffer.vk_buffer](const VkCommandBuffer vk_command_buffer) { + target_image->TransitImageLayout(vk_command_buffer, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + target_image->CopyFromBuffer(vk_command_buffer, staging_vk_buffer); + target_image->GenerateMipmaps(vk_command_buffer); + target_image->TransitImageLayout(vk_command_buffer, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + }); + gpu_service.ReleaseStagingBuffer(staging_buffer); + } catch (...) { + gpu_service.ReleaseStagingBuffer(staging_buffer); + throw; + } + }); + } catch (...) { + pending_counter->fetch_sub(1); + throw; + } +} + void Texture2DStorage::UploadData(const std::vector& data, const glm::uvec2& resolution) { if (!Platform::Initialized()) return; @@ -303,13 +404,31 @@ void Texture2DStorage::SetData(const std::vector& data, const glm::uv } void Texture2DStorage::UploadDataImmediately() { - SetDataImmediately(new_data_, new_resolution_); + if (new_data_.empty()) { + return; + } + const auto upload = SetDataAsync(new_data_, new_resolution_); + new_data_.clear(); + new_resolution_ = {}; + if (upload.Valid()) { + Platform::GetGpuService().Wait(upload); + } } uint32_t TextureStorage::GetVersion() { return GetInstance().version_; } +bool TextureStorage::HasPendingUploads() { + const auto& storage = GetInstance(); + for (const auto& texture_storage : storage.texture_2ds_) { + if (!texture_storage.new_data_.empty() || texture_storage.IsGpuUploadPending()) { + return true; + } + } + return false; +} + void TextureStorage::DeviceSync() { if (!Platform::Initialized()) return; @@ -326,7 +445,7 @@ void TextureStorage::DeviceSync() { for (int texture_index = 0; texture_index < storage.texture_2ds_.size(); texture_index++) { if (auto& texture_storage = storage.texture_2ds_[texture_index]; !texture_storage.new_data_.empty()) { - texture_storage.UploadData(texture_storage.new_data_, texture_storage.new_resolution_); + (void)texture_storage.SetDataAsync(texture_storage.new_data_, texture_storage.new_resolution_); texture_storage.new_data_.clear(); texture_storage.new_resolution_ = {}; storage.version_++; @@ -360,6 +479,8 @@ void TextureStorage::BindTexture2DToDescriptorSet(const std::shared_ptr>& future, + const std::chrono::milliseconds timeout) { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) { + AssetManager::ExecuteMainThreadAssetTasks(1); + Jobs::ExecuteMainThreadJobs(1); + if (future.wait_for(10ms) == std::future_status::ready) { + return true; + } + } + return false; +} + +class ScopedGpuPendingWorkRelease { + public: + ~ScopedGpuPendingWorkRelease() { + ReleaseGpuPendingWork(); + } +}; + +class GpuPendingLoadPayload final : public StagedAssetLoadPayload {}; + +class GpuPendingLoadAsset final : public IAsset { + protected: + [[nodiscard]] bool SupportsStagedLoading() const override { + return true; + } + + [[nodiscard]] std::shared_ptr LoadStagedPayloadInternal( + const std::filesystem::path&) const override { + auto& state = GetGpuPendingLoadState(); + std::lock_guard lock(state.mutex); + ++state.payload_load_count; + return std::make_shared(); + } + + bool ApplyStagedPayloadInternal(const std::filesystem::path&, + const std::shared_ptr& payload) override { + if (!std::dynamic_pointer_cast(payload)) { + return false; + } + { + auto& state = GetGpuPendingLoadState(); + std::lock_guard lock(state.mutex); + ++state.finalize_count; + } + + const auto handle = Jobs::RunOnBackgroundThread([]() { + { + auto& state = GetGpuPendingLoadState(); + std::lock_guard lock(state.mutex); + ++state.gpu_work_start_count; + state.gpu_work_started = true; + state.gpu_work_ran_on_background = Jobs::IsExecutorThread(JobExecutorType::Background); + } + GetGpuPendingLoadState().cv.notify_all(); + + auto& state = GetGpuPendingLoadState(); + std::unique_lock lock(state.mutex); + state.cv.wait(lock, [&]() { + return state.release_gpu_work; + }); + const bool should_fail = state.fail_gpu_work; + if (should_fail) { + throw std::runtime_error("gpu readiness failure"); + } + ++state.gpu_work_complete_count; + }); + Jobs::Execute(handle); + TrackPendingGpuWork(handle); + return true; + } +}; + class TempProject { public: TempProject() { @@ -301,6 +434,21 @@ void WriteStagedAssetFixture(const TempProject& project) { metadata_file << "asset_handle_: " << kStagedAssetHandle << "\n"; } +void WriteGpuPendingAssetFixture(const TempProject& project) { + const auto asset_path = project.AssetsPath() / ("GpuPending" + std::string(kGpuPendingAssetExtension)); + { + std::ofstream asset_file(asset_path); + asset_file << "gpu pending asset payload"; + } + + const auto metadata_path = asset_path.string() + ".evefilemeta"; + std::ofstream metadata_file(metadata_path); + metadata_file << "asset_extension_: " << kGpuPendingAssetExtension << "\n"; + metadata_file << "asset_file_name_: GpuPending\n"; + metadata_file << "asset_type_name_: " << kGpuPendingAssetTypeName << "\n"; + metadata_file << "asset_handle_: " << kGpuPendingAssetHandle << "\n"; +} + ApplicationInitializationSettings TestApplicationSettings(const TempProject& project) { ApplicationInitializationSettings settings; settings.project_path = project.ProjectPath(); @@ -424,3 +572,82 @@ TEST(AssetManager, AsyncStagedLoadSeparatesAssetIoFromMainThreadFinalization) { const auto later_asset = AssetManager::GetAsset(Handle(kStagedAssetHandle)); EXPECT_EQ(later_asset, asset); } + +TEST(AssetManager, AsyncStagedLoadWaitsForGpuReadinessBeforePublishing) { + ResetGpuPendingLoadState(); + TempProject project; + WriteGpuPendingAssetFixture(project); + + Application app; + ApplicationContextScope scope(app); + app.RegisterAsset(kGpuPendingAssetTypeName, {kGpuPendingAssetExtension}); + auto settings = TestApplicationSettings(project); + settings.load_project_assets = false; + app.Initialize(settings); + + auto asset_future = AssetManager::RequestAssetLoad(Handle(kGpuPendingAssetHandle)); + const bool gpu_work_started = WaitForGpuPendingWorkStartedWhilePumping(5s); + if (!gpu_work_started) { + ReleaseGpuPendingWork(); + FAIL() << "Timed out waiting for simulated GPU readiness work to start."; + } + ScopedGpuPendingWorkRelease release_on_exit; + + const auto gpu_pending_snapshot = AssetManager::GetAssetLoadSnapshot(); + EXPECT_TRUE(gpu_pending_snapshot.Active()); + EXPECT_EQ(gpu_pending_snapshot.active_state, AssetManager::AssetLoadState::GpuPending); + EXPECT_EQ(gpu_pending_snapshot.gpu_pending, 1); + EXPECT_EQ(asset_future.wait_for(100ms), std::future_status::timeout); + + auto sync_access = std::async(std::launch::async, [&]() { + ApplicationContextScope thread_scope(app); + return AssetManager::GetAsset(Handle(kGpuPendingAssetHandle)); + }); + EXPECT_EQ(sync_access.wait_for(100ms), std::future_status::timeout); + + ReleaseGpuPendingWork(); + ASSERT_TRUE(WaitForAssetFutureReadyWhilePumping(asset_future, 5s)); + const auto asset = std::dynamic_pointer_cast(asset_future.get()); + ASSERT_NE(asset, nullptr); + EXPECT_EQ(sync_access.get(), asset); + + const auto final_snapshot = AssetManager::GetAssetLoadSnapshot(); + EXPECT_FALSE(final_snapshot.Active()); + EXPECT_EQ(final_snapshot.completed, 1); + + auto& state = GetGpuPendingLoadState(); + std::lock_guard lock(state.mutex); + EXPECT_EQ(state.payload_load_count, 1); + EXPECT_EQ(state.finalize_count, 1); + EXPECT_EQ(state.gpu_work_start_count, 1); + EXPECT_EQ(state.gpu_work_complete_count, 1); + EXPECT_TRUE(state.gpu_work_ran_on_background); +} + +TEST(AssetManager, AsyncStagedLoadPropagatesGpuReadinessFailure) { + ResetGpuPendingLoadState(true); + TempProject project; + WriteGpuPendingAssetFixture(project); + + Application app; + ApplicationContextScope scope(app); + app.RegisterAsset(kGpuPendingAssetTypeName, {kGpuPendingAssetExtension}); + auto settings = TestApplicationSettings(project); + settings.load_project_assets = false; + app.Initialize(settings); + + auto asset_future = AssetManager::RequestAssetLoad(Handle(kGpuPendingAssetHandle)); + const bool gpu_work_started = WaitForGpuPendingWorkStartedWhilePumping(5s); + if (!gpu_work_started) { + ReleaseGpuPendingWork(); + FAIL() << "Timed out waiting for simulated GPU readiness work to start."; + } + + ReleaseGpuPendingWork(); + ASSERT_TRUE(WaitForAssetFutureReadyWhilePumping(asset_future, 5s)); + EXPECT_THROW((void)asset_future.get(), std::runtime_error); + + const auto final_snapshot = AssetManager::GetAssetLoadSnapshot(); + EXPECT_FALSE(final_snapshot.Active()); + EXPECT_EQ(final_snapshot.failed, 1); +} diff --git a/EvoEngine_Tests/Core/GpuServiceTest.cpp b/EvoEngine_Tests/Core/GpuServiceTest.cpp index f6727f76..03f50a51 100644 --- a/EvoEngine_Tests/Core/GpuServiceTest.cpp +++ b/EvoEngine_Tests/Core/GpuServiceTest.cpp @@ -7,8 +7,10 @@ #include "GpuService.hpp" #include "GraphicsResources.hpp" #include "Jobs.hpp" +#include "Mesh.hpp" #include "Platform.hpp" #include "RenderLayer.hpp" +#include "Texture2D.hpp" #include #include @@ -29,6 +31,10 @@ class PlatformLifecycleTestAccess final { Platform::Initialize(settings); } + static void PreUpdate() { + Platform::PreUpdate(); + } + static void OnDestroy() { Platform::OnDestroy(); } @@ -257,6 +263,120 @@ TEST(GpuService, BufferUploadReadbackRoundTrip) { } } +TEST(GpuService, Texture2DAsyncUploadProducesReadyImage) { + ScopedGpuPlatform platform; + auto& gpu_service = Platform::GetGpuService(); + + Texture2D texture; + std::vector pixels = { + glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), + glm::vec4(0.0f, 1.0f, 0.0f, 1.0f), + glm::vec4(0.0f, 0.0f, 1.0f, 1.0f), + glm::vec4(1.0f, 1.0f, 1.0f, 1.0f), + }; + auto& texture_storage = texture.RefTexture2DStorage(); + + const auto upload = texture_storage.SetDataAsync(pixels, glm::uvec2(2, 2)); + ASSERT_TRUE(upload.Valid()); + EXPECT_TRUE(texture_storage.IsGpuUploadPending()); + gpu_service.Wait(upload); + + EXPECT_FALSE(texture_storage.IsGpuUploadPending()); + EXPECT_NE(texture_storage.GetVkImage(), VK_NULL_HANDLE); + EXPECT_NE(texture_storage.GetVkImageView(), VK_NULL_HANDLE); + EXPECT_NE(texture_storage.GetVkSampler(), VK_NULL_HANDLE); + EXPECT_EQ(texture_storage.GetLayout(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); +} + +TEST(GpuService, Texture2DRuntimeUpdateTracksGpuReadiness) { + ScopedGpuPlatform platform; + + Texture2D texture; + const std::vector pixels = { + glm::vec4(0.2f, 0.0f, 0.0f, 1.0f), + glm::vec4(0.0f, 0.4f, 0.0f, 1.0f), + glm::vec4(0.0f, 0.0f, 0.6f, 1.0f), + glm::vec4(0.8f, 0.8f, 0.8f, 1.0f), + }; + auto& texture_storage = texture.RefTexture2DStorage(); + + texture.SetRgbaChannelData(pixels, glm::uvec2(2, 2)); + EXPECT_TRUE(texture.HasPendingGpuWork()); + texture.WaitForPendingGpuWork(); + + EXPECT_FALSE(texture.HasPendingGpuWork()); + EXPECT_FALSE(texture_storage.IsGpuUploadPending()); + EXPECT_EQ(texture_storage.GetLayout(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + std::vector readback; + texture.GetRgbaChannelData(readback); + ASSERT_EQ(readback.size(), pixels.size()); + for (size_t i = 0; i < pixels.size(); ++i) { + EXPECT_NEAR(readback[i].r, pixels[i].r, 0.001f); + EXPECT_NEAR(readback[i].g, pixels[i].g, 0.001f); + EXPECT_NEAR(readback[i].b, pixels[i].b, 0.001f); + EXPECT_NEAR(readback[i].a, pixels[i].a, 0.001f); + } +} + +TEST(GpuService, TextureStorageDeviceSyncUsesAsyncUploadPath) { + ScopedGpuPlatform platform; + auto& gpu_service = Platform::GetGpuService(); + + Texture2D texture; + const std::vector pixels = { + glm::vec4(1.0f, 0.25f, 0.0f, 1.0f), + glm::vec4(0.0f, 1.0f, 0.25f, 1.0f), + glm::vec4(0.25f, 0.0f, 1.0f, 1.0f), + glm::vec4(1.0f, 1.0f, 0.25f, 1.0f), + }; + auto& texture_storage = texture.RefTexture2DStorage(); + + texture_storage.SetData(pixels, glm::uvec2(2, 2)); + TextureStorage::DeviceSync(); + gpu_service.WaitIdle(); + + EXPECT_FALSE(texture_storage.IsGpuUploadPending()); + EXPECT_NE(texture_storage.GetVkImage(), VK_NULL_HANDLE); + EXPECT_NE(texture_storage.GetVkImageView(), VK_NULL_HANDLE); + EXPECT_NE(texture_storage.GetVkSampler(), VK_NULL_HANDLE); + EXPECT_EQ(texture_storage.GetLayout(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); +} + +TEST(GpuService, GeometryStorageCommitsMeshRangesAfterAsyncUpload) { + ScopedGpuPlatform platform; + auto& gpu_service = Platform::GetGpuService(); + + Mesh mesh; + mesh.OnCreate(); + VertexAttributes attributes{}; + attributes.normal = true; + attributes.tangent = true; + std::vector vertices(3); + vertices[0].position = glm::vec3(0.0f, 0.0f, 0.0f); + vertices[1].position = glm::vec3(1.0f, 0.0f, 0.0f); + vertices[2].position = glm::vec3(0.0f, 1.0f, 0.0f); + vertices[0].normal = vertices[1].normal = vertices[2].normal = glm::vec3(0.0f, 0.0f, 1.0f); + vertices[0].tangent = vertices[1].tangent = vertices[2].tangent = glm::vec3(1.0f, 0.0f, 0.0f); + const std::vector triangles = {glm::uvec3(0, 1, 2)}; + + mesh.SetVertices(attributes, vertices, triangles); + const auto& triangle_range = mesh.GetTriangleRange(); + ASSERT_TRUE(triangle_range); + EXPECT_EQ(triangle_range->prev_frame_index_count, 0u); + EXPECT_EQ(triangle_range->prev_frame_range, 0u); + + const auto version_before_sync = GeometryStorage::GetVersion(); + PlatformLifecycleTestAccess::PreUpdate(); + gpu_service.WaitIdle(); + PlatformLifecycleTestAccess::PreUpdate(); + + EXPECT_GT(GeometryStorage::GetVersion(), version_before_sync); + EXPECT_EQ(triangle_range->prev_frame_index_count, 1u); + EXPECT_EQ(triangle_range->prev_frame_range, triangle_range->range); + EXPECT_EQ(triangle_range->prev_frame_offset, triangle_range->offset); +} + TEST(GpuService, ConcurrentBufferUploadsRoundTrip) { ScopedGpuPlatform platform; auto& gpu_service = Platform::GetGpuService(); diff --git a/EvoEngine_Tests/Core/TaskRuntimeTest.cpp b/EvoEngine_Tests/Core/TaskRuntimeTest.cpp index ef6535c1..35278843 100644 --- a/EvoEngine_Tests/Core/TaskRuntimeTest.cpp +++ b/EvoEngine_Tests/Core/TaskRuntimeTest.cpp @@ -61,6 +61,17 @@ TEST(TaskRuntime, RunsMainThreadTasksOnWaitingMainThread) { EXPECT_EQ(executed_thread_id, main_thread_id); } +TEST(TaskRuntime, CompletedHandleRemainsCompletedAfterWaitRecyclesIt) { + TaskRuntime runtime; + runtime.Initialize(TestRuntimeSettings()); + + const auto task = runtime.Schedule([]() { + }); + + runtime.Wait(task); + EXPECT_TRUE(runtime.IsCompleted(task)); +} + TEST(TaskRuntime, RunsNamedServiceExecutors) { TaskRuntime runtime; runtime.Initialize(TestRuntimeSettings()); diff --git a/PythonBinding/src/PyEvoEngine.cpp b/PythonBinding/src/PyEvoEngine.cpp index 828bc234..1a29db72 100644 --- a/PythonBinding/src/PyEvoEngine.cpp +++ b/PythonBinding/src/PyEvoEngine.cpp @@ -1,4 +1,6 @@ #include "PyEvoEngine.hpp" +#include "GeometryStorage.hpp" +#include "TextureStorage.hpp" #ifdef CUDA_MODULE_SERVICE # include "RayTracerLayer.hpp" #endif @@ -48,7 +50,45 @@ bool PyEvoEngine::CaptureCurrentScene(const int resolution_x, const int resoluti return false; } - const auto scene = ApplicationContext::Get().GetActiveScene(); + constexpr int max_readiness_frames = 300; + int readiness_frames = 0; + auto& application = ApplicationContext::Get(); + const auto is_scene_ready = []() { + return ProjectManager::IsProjectIdle() && !GeometryStorage::HasPendingUploads() && + !TextureStorage::HasPendingUploads(); + }; + while (!is_scene_ready() && readiness_frames < max_readiness_frames) { + application.Loop(); + readiness_frames++; + } + if (!is_scene_ready()) { + const auto snapshot = AssetManager::GetAssetLoadSnapshot(); + const auto geometry_snapshot = GeometryStorage::GetUploadSnapshot(); + EVOENGINE_ERROR("Scene is not ready for capture! Frames: " + std::to_string(readiness_frames) + + ", project idle: " + std::to_string(ProjectManager::IsProjectIdle()) + + ", geometry version: " + std::to_string(GeometryStorage::GetVersion()) + + ", texture pending: " + std::to_string(TextureStorage::HasPendingUploads()) + + ", mesh dirty/active/completed/handles: " + std::to_string(geometry_snapshot.mesh_dirty) + "/" + + std::to_string(geometry_snapshot.mesh_upload_active) + "/" + + std::to_string(geometry_snapshot.mesh_upload_completed) + "/" + + std::to_string(geometry_snapshot.mesh_upload_handles) + + ", skinned dirty/active/completed/handles: " + + std::to_string(geometry_snapshot.skinned_mesh_dirty) + "/" + + std::to_string(geometry_snapshot.skinned_mesh_upload_active) + "/" + + std::to_string(geometry_snapshot.skinned_mesh_upload_completed) + "/" + + std::to_string(geometry_snapshot.skinned_mesh_upload_handles) + + ", strand dirty/active/completed/handles: " + std::to_string(geometry_snapshot.strand_dirty) + + "/" + std::to_string(geometry_snapshot.strand_upload_active) + "/" + + std::to_string(geometry_snapshot.strand_upload_completed) + "/" + + std::to_string(geometry_snapshot.strand_upload_handles) + + ", asset queued: " + std::to_string(snapshot.queued) + + ", asset loading CPU: " + std::to_string(snapshot.loading_cpu) + + ", asset waiting finalize: " + std::to_string(snapshot.waiting_for_finalize) + + ", asset GPU pending: " + std::to_string(snapshot.gpu_pending)) + return false; + } + + const auto scene = application.GetActiveScene(); if (!scene) { EVOENGINE_ERROR("No active scene!"); return false; @@ -61,7 +101,7 @@ bool PyEvoEngine::CaptureCurrentScene(const int resolution_x, const int resoluti main_camera->Resize({resolution_x, resolution_y}); const auto loop_count = std::max(1, warmup_frames); for (int i = 0; i < loop_count; i++) { - ApplicationContext::Get().Loop(); + application.Loop(); } if (const auto parent_path = output_path.parent_path(); !parent_path.empty()) { std::filesystem::create_directories(parent_path); diff --git a/README.md b/README.md index 02923e2b..2f65c379 100644 --- a/README.md +++ b/README.md @@ -93,9 +93,9 @@ Services can extend rendering through `RenderLayer` callbacks for shadow maps, d The SDK job system supports scheduled and immediate parallel work. ECS iteration helpers use jobs to process chunks in parallel. The runtime owns a general worker pool plus named service executors for main-thread callbacks, asset IO, GPU resource work, future rendering work, and background tasks. `JobSystem` and `Jobs` remain compatibility facades over this engine-owned `TaskRuntime` boundary so backend experiments can happen behind the same API. -GPU upload/finalization work that is not full frame rendering is routed through the platform-owned `GpuService` instance on a dedicated GPU executor. `Platform::ImmediateSubmit` forwards into that service, and buffers expose async upload/readback entry points for callers that can keep CPU work moving while GPU transfer work completes. The application frame loop, `RenderLayer::RenderAll`, window rendering, and presentation still run on the main thread; the render executor is reserved for future render-thread work. +GPU upload/finalization work that is not full frame rendering is routed through the platform-owned `GpuService` instance on a dedicated GPU executor. `Platform::ImmediateSubmit` forwards into that service, and buffers expose async upload/readback entry points for callers that can keep CPU work moving while GPU transfer work completes. Geometry storage schedules whole-buffer mesh, skinned mesh, and strand uploads on this path and publishes draw ranges only after those uploads complete; partial range updates are still future work. The application frame loop, `RenderLayer::RenderAll`, window rendering, and presentation still run on the main thread; the render executor is reserved for future render-thread work. -Interactive project asset loading now dispatches scanned assets as an `AssetManager` batch instead of resolving one pending asset per frame. The asset-service path owns per-handle in-flight state, progress snapshots, and blocking sync access, so concurrent requests wait for the first loader instead of creating duplicate asset instances. Assets can opt into staged async loading by producing a CPU-only payload on the asset-IO executor and applying it later on the bounded main-thread finalization lane. SDK staged assets now include `Texture2D`, shader/JSON source assets, scene and native prefab YAML, strands, point clouds, and the SDK YAML-backed render assets that deserialize through the default `IAsset` path. Legacy non-staged paths, especially imported model prefab formats that still run through Assimp and create engine objects during import, continue to finalize on the main thread. `ApplicationInitializationSettings::load_project_assets` can disable headless project asset preloading for tests and tools that want to request assets explicitly. Input is routed from GLFW callbacks through the engine input system and layer event hooks. Timing utilities track frame delta time, fixed timestep state, and update counters. +Interactive project asset loading now dispatches scanned assets as an `AssetManager` batch instead of resolving one pending asset per frame. The asset-service path owns per-handle in-flight state, progress snapshots, and blocking sync access, so concurrent requests wait for the first loader instead of creating duplicate asset instances. Assets can opt into staged async loading by producing a CPU-only payload on the asset-IO executor and applying it later on the bounded main-thread finalization lane. GPU-backed staged assets can also register GPU readiness work; those loads remain `GpuPending` until the tracked GPU jobs complete, and sync access blocks until the asset is fully ready. `Texture2D` uses this path so staged pixel decoding is separated from asynchronous GPU upload, and runtime texture data updates use the same GPU-service-backed upload path while readback/export waits for pending texture GPU work. SDK staged assets also include shader/JSON source assets, scene and native prefab YAML, strands, point clouds, and the SDK YAML-backed render assets that deserialize through the default `IAsset` path. Legacy non-staged paths, especially imported model prefab formats that still run through Assimp and create engine objects during import, continue to finalize on the main thread. `ApplicationInitializationSettings::load_project_assets` can disable headless project asset preloading for tests and tools that want to request assets explicitly. Input is routed from GLFW callbacks through the engine input system and layer event hooks. Timing utilities track frame delta time, fixed timestep state, and update counters. ### Applications From bffc1fbb5397864a339eb0d0dc8403d7ee5ab715 Mon Sep 17 00:00:00 2001 From: Bosheng Li Date: Thu, 4 Jun 2026 17:52:28 -0700 Subject: [PATCH 2/3] Refine GPU upload readiness plumbing --- EvoEngine_SDK/include/Core/ECS/IAsset.hpp | 19 +- .../Rendering/Geometry/GeometryStorage.hpp | 57 ++-- .../Rendering/Platform/GraphicsResources.hpp | 25 +- .../include/Rendering/Platform/Platform.hpp | 5 + .../Rendering/Texture/TextureStorage.hpp | 19 +- EvoEngine_SDK/src/AssetManager.cpp | 126 ++++----- EvoEngine_SDK/src/GeometryStorage.cpp | 263 ++++++++++++------ EvoEngine_SDK/src/GraphicsResources.cpp | 42 ++- EvoEngine_SDK/src/IAsset.cpp | 23 +- EvoEngine_SDK/src/Platform.cpp | 13 + EvoEngine_SDK/src/RenderLayer.cpp | 11 +- EvoEngine_SDK/src/Resources.cpp | 10 +- EvoEngine_SDK/src/Texture2D.cpp | 7 +- EvoEngine_SDK/src/TextureStorage.cpp | 80 +----- EvoEngine_Tests/Core/GpuServiceTest.cpp | 83 +++++- PythonBinding/src/PyEvoEngine.cpp | 15 +- README.md | 2 +- 17 files changed, 447 insertions(+), 353 deletions(-) diff --git a/EvoEngine_SDK/include/Core/ECS/IAsset.hpp b/EvoEngine_SDK/include/Core/ECS/IAsset.hpp index 7fc432d0..d865e45d 100644 --- a/EvoEngine_SDK/include/Core/ECS/IAsset.hpp +++ b/EvoEngine_SDK/include/Core/ECS/IAsset.hpp @@ -103,25 +103,20 @@ class IAsset : public ISerializable { */ void TrackPendingGpuWork(const JobHandle& handle); - bool saved_ = false; /**< Indicates whether the asset is in a saved state. */ - uint32_t version_ = 0; /**< The version number of the asset. */ - - public: /** - * @brief Returns whether this asset has valid tracked GPU work that has not been consumed by a readiness wait. + * @brief Takes tracked GPU work handles for readiness orchestration. */ - [[nodiscard]] bool HasPendingGpuWork() const; + [[nodiscard]] std::vector ConsumePendingGpuWorkHandles() const; /** - * @brief Returns tracked GPU work handles for readiness orchestration. - */ - [[nodiscard]] std::vector GetPendingGpuWorkHandles() const; - - /** - * @brief Waits for all tracked GPU work and clears completed readiness handles. + * @brief Waits for tracked GPU work and clears consumed readiness handles. */ void WaitForPendingGpuWork() const; + bool saved_ = false; /**< Indicates whether the asset is in a saved state. */ + uint32_t version_ = 0; /**< The version number of the asset. */ + + public: /** * @brief Generates a thumbnail texture for the asset. * @return A shared pointer to the generated thumbnail texture. diff --git a/EvoEngine_SDK/include/Rendering/Geometry/GeometryStorage.hpp b/EvoEngine_SDK/include/Rendering/Geometry/GeometryStorage.hpp index bef5239c..5d9b8b08 100644 --- a/EvoEngine_SDK/include/Rendering/Geometry/GeometryStorage.hpp +++ b/EvoEngine_SDK/include/Rendering/Geometry/GeometryStorage.hpp @@ -3,6 +3,8 @@ #include "Platform.hpp" #include "Vertex.hpp" +#include + namespace evo_engine { /** @@ -213,24 +215,6 @@ struct ParticleInfoListData { class GeometryStorage final { public: static GeometryStorage& GetInstance(); - struct GeometryUploadSnapshot { - bool mesh_dirty = false; - bool mesh_upload_active = false; - bool mesh_upload_completed = true; - size_t mesh_upload_handles = 0; - - bool skinned_mesh_dirty = false; - bool skinned_mesh_upload_active = false; - bool skinned_mesh_upload_completed = true; - size_t skinned_mesh_upload_handles = 0; - - bool strand_dirty = false; - bool strand_upload_active = false; - bool strand_upload_completed = true; - size_t strand_upload_handles = 0; - - [[nodiscard]] bool Active() const; - }; private: struct RangeCommit { @@ -247,6 +231,25 @@ class GeometryStorage final { std::vector index_commits; }; + struct DirtyRange { + bool dirty = false; + size_t begin = 0; + size_t end = 0; + + void Mark(size_t range_begin, size_t count); + void MarkTail(size_t range_begin, size_t range_end); + void Clear(); + [[nodiscard]] bool Empty() const; + }; + + struct DirtyBufferUpload { + const std::shared_ptr* buffer = nullptr; + const void* data = nullptr; + size_t element_count = 0; + size_t element_size = 0; + DirtyRange* dirty_range = nullptr; + }; + std::vector vertex_data_chunks_ = {}; std::vector meshlets_ = {}; std::vector> meshlet_range_descriptor_; @@ -258,6 +261,9 @@ class GeometryStorage final { std::shared_ptr triangle_buffer_ = {}; bool require_mesh_data_device_update_ = {}; PendingGeometryUpload pending_mesh_upload_; + DirtyRange mesh_vertex_dirty_range_; + DirtyRange meshlet_dirty_range_; + DirtyRange triangle_dirty_range_; std::vector skinned_vertex_data_chunks_ = {}; std::vector skinned_meshlets_ = {}; @@ -270,6 +276,9 @@ class GeometryStorage final { std::shared_ptr skinned_triangle_buffer_ = {}; bool require_skinned_mesh_data_device_update_ = {}; PendingGeometryUpload pending_skinned_mesh_upload_; + DirtyRange skinned_vertex_dirty_range_; + DirtyRange skinned_meshlet_dirty_range_; + DirtyRange skinned_triangle_dirty_range_; std::vector strand_point_data_chunks_ = {}; std::vector strand_meshlets_ = {}; @@ -282,8 +291,15 @@ class GeometryStorage final { std::shared_ptr segment_buffer_ = {}; bool require_strand_mesh_data_device_update_ = {}; PendingGeometryUpload pending_strand_upload_; + DirtyRange strand_point_dirty_range_; + DirtyRange strand_meshlet_dirty_range_; + DirtyRange segment_dirty_range_; void UploadData(); + void ClearMeshDirtyRanges(); + void ClearSkinnedMeshDirtyRanges(); + void ClearStrandDirtyRanges(); + static GpuWorkHandle ScheduleDirtyBufferUpload(const DirtyBufferUpload& upload); static void CaptureRangeCommits(const std::vector>& descriptors, std::vector& commits); static void ApplyRangeCommits(const std::vector& commits); @@ -293,6 +309,10 @@ class GeometryStorage final { static void ClearPendingUpload(PendingGeometryUpload& upload); bool CompletePendingUpload(PendingGeometryUpload& upload); void CompletePendingUploads(); + void ScheduleUploadGroup(bool& dirty, PendingGeometryUpload& pending_upload, + std::initializer_list uploads, + const std::vector>& meshlet_descriptors, + const std::vector>& index_descriptors); void SchedulePendingUploads(); friend class RenderLayer; friend class Resources; @@ -306,7 +326,6 @@ class GeometryStorage final { public: [[nodiscard]] static uint32_t GetVersion(); - [[nodiscard]] static GeometryUploadSnapshot GetUploadSnapshot(); [[nodiscard]] static bool HasPendingUploads(); static void WaitForPendingUploads(); static const std::shared_ptr& GetTriangleBuffer(); diff --git a/EvoEngine_SDK/include/Rendering/Platform/GraphicsResources.hpp b/EvoEngine_SDK/include/Rendering/Platform/GraphicsResources.hpp index 5176fabc..632396d5 100644 --- a/EvoEngine_SDK/include/Rendering/Platform/GraphicsResources.hpp +++ b/EvoEngine_SDK/include/Rendering/Platform/GraphicsResources.hpp @@ -494,7 +494,8 @@ class Buffer final : public IGraphicsResource { const VmaAllocationCreateInfo& vma_allocation_create_info); static void ResizeOnGpuThread(const std::shared_ptr& state, VkDeviceSize new_size); static void DestroyOnGpuThread(const std::shared_ptr& state); - static void UploadDataOnGpuThread(const std::shared_ptr& state, size_t size, const void* src); + static void UploadDataOnGpuThread(const std::shared_ptr& state, size_t size, const void* src, + VkDeviceSize dst_offset); static void DownloadDataOnGpuThread(const std::shared_ptr& state, size_t size, void* dst); static void CopyFromBufferOnGpuThread(const std::shared_ptr& state, const std::shared_ptr& src_state, VkDeviceSize size, @@ -537,6 +538,14 @@ class Buffer final : public IGraphicsResource { */ void UploadData(size_t size, const void* src); + /** + * @brief Uploads data into an existing buffer subrange. + * @param size Size of the data to upload. + * @param src Pointer to the data source. + * @param dst_offset Destination byte offset. + */ + void UploadSubData(size_t size, const void* src, VkDeviceSize dst_offset); + /** * @brief Enqueues an asynchronous upload to the buffer. * @param size Size of the data to upload. @@ -544,6 +553,14 @@ class Buffer final : public IGraphicsResource { */ [[nodiscard]] GpuWorkHandle UploadDataAsync(size_t size, const void* src); + /** + * @brief Enqueues an asynchronous upload into an existing buffer subrange. + * @param size Size of the data to upload. + * @param src Pointer to the data source. The data is copied before this function returns. + * @param dst_offset Destination byte offset. + */ + [[nodiscard]] GpuWorkHandle UploadSubDataAsync(size_t size, const void* src, VkDeviceSize dst_offset); + /** * @brief Downloads data from the buffer. * @param size Size of the data to download. @@ -660,6 +677,12 @@ class Buffer final : public IGraphicsResource { */ [[nodiscard]] const VkBuffer& GetVkBuffer() const; + /** + * @brief Retrieves the current allocated buffer size in bytes. + * @return Current buffer size in bytes. + */ + [[nodiscard]] VkDeviceSize GetSize() const; + /** * @brief Retrieves the VMA allocation handle for the buffer. * @return VMA allocation handle. diff --git a/EvoEngine_SDK/include/Rendering/Platform/Platform.hpp b/EvoEngine_SDK/include/Rendering/Platform/Platform.hpp index de6b8e7c..34cdc659 100644 --- a/EvoEngine_SDK/include/Rendering/Platform/Platform.hpp +++ b/EvoEngine_SDK/include/Rendering/Platform/Platform.hpp @@ -454,6 +454,11 @@ class Platform final { */ static void WaitForDeviceIdle(); + /** + * @brief Drains pending resource upload work and waits for GPU/device idle. + */ + static void DrainGpuResourceWork(); + /// List of primitive counts for debugging purposes. std::vector prim_count{}; diff --git a/EvoEngine_SDK/include/Rendering/Texture/TextureStorage.hpp b/EvoEngine_SDK/include/Rendering/Texture/TextureStorage.hpp index 88663012..85150976 100644 --- a/EvoEngine_SDK/include/Rendering/Texture/TextureStorage.hpp +++ b/EvoEngine_SDK/include/Rendering/Texture/TextureStorage.hpp @@ -20,6 +20,7 @@ struct TextureStorageHandle { */ class Texture2DStorage { friend class TextureStorage; + friend class Texture2D; friend class Cubemap; /** @@ -33,11 +34,9 @@ class Texture2DStorage { glm::uvec2 new_resolution_{}; /** - * @brief Uploads texture data to the GPU. - * @param data The pixel data to upload. - * @param resolution The resolution of the texture. + * @brief Immediately uploads any pending data to the GPU. */ - void UploadData(const std::vector& data, const glm::uvec2& resolution); + void UploadPendingDataImmediately(); public: bool pending_delete = false; ///< Indicates whether the storage is pending deletion. @@ -93,13 +92,6 @@ class Texture2DStorage { */ void Initialize(const glm::uvec2& resolution); - /** - * @brief Sets texture data and uploads it immediately to the GPU. - * @param data The pixel data to set. - * @param resolution The resolution of the texture. - */ - void SetDataImmediately(const std::vector& data, const glm::uvec2& resolution); - /** * @brief Sets texture data and uploads it asynchronously through the GPU service. * @param data The pixel data to upload. @@ -115,11 +107,6 @@ class Texture2DStorage { */ void SetData(const std::vector& data, const glm::uvec2& resolution); - /** - * @brief Immediately uploads any pending data to the GPU. - */ - void UploadDataImmediately(); - /** * @brief Clears the texture resources and data. */ diff --git a/EvoEngine_SDK/src/AssetManager.cpp b/EvoEngine_SDK/src/AssetManager.cpp index d57607cc..093483c2 100644 --- a/EvoEngine_SDK/src/AssetManager.cpp +++ b/EvoEngine_SDK/src/AssetManager.cpp @@ -404,73 +404,67 @@ void AssetManager::StartAssetServiceLoadImpl(const Handle& asset_handle, throw std::runtime_error("Failed to build staged asset payload."); } UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::WaitingForFinalize, "Waiting for asset finalization."); - ScheduleMainThreadAssetTaskImpl( - [asset_handle, context = std::move(context), promise, payload = std::move(payload)]() { - try { - if (!context.asset->ApplyStagedPayloadInternal(context.absolute_path, payload)) { - throw std::runtime_error("Failed to apply staged asset load payload."); - } - context.asset->saved_ = true; - auto publish_loaded_asset = [asset_handle, context, promise]() { - context.file->asset_ = context.asset; - { - auto& asset_manager = GetInstance(); - std::lock_guard lock(asset_manager.asset_registry_.asset_registry_mutex); - asset_manager.asset_registry_.assets_[asset_handle] = context.asset; - } - promise->set_value(context.asset); - UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Loaded, "Asset loaded."); - FinishAssetLoadingImpl(asset_handle); - }; - if (context.asset->HasPendingGpuWork()) { - UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::GpuPending, "Waiting for GPU finalization."); - JobOptions gpu_ready_options; - gpu_ready_options.executor = JobExecutorType::Background; - gpu_ready_options.affinity = JobThreadAffinity::Background; - gpu_ready_options.debug_name = "AssetManager::WaitForAssetGpuReady"; - const auto gpu_ready_handle = Jobs::Run( - context.asset->GetPendingGpuWorkHandles(), gpu_ready_options, [asset_handle, context, promise]() { - try { - context.asset->WaitForPendingGpuWork(); - ScheduleMainThreadAssetTaskImpl([asset_handle, context, promise]() { - context.file->asset_ = context.asset; - { - auto& asset_manager = GetInstance(); - std::lock_guard lock(asset_manager.asset_registry_.asset_registry_mutex); - asset_manager.asset_registry_.assets_[asset_handle] = context.asset; - } - promise->set_value(context.asset); - UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Loaded, "Asset loaded."); - FinishAssetLoadingImpl(asset_handle); - }); - } catch (const std::exception& e) { - promise->set_exception(std::current_exception()); - UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, e.what()); - FinishAssetLoadingImpl(asset_handle); - } catch (...) { - promise->set_exception(std::current_exception()); - UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, - "Unknown asset GPU finalization failure."); - FinishAssetLoadingImpl(asset_handle); - } - }); - if (!gpu_ready_handle.Valid()) { - throw std::runtime_error("Failed to schedule asset GPU readiness wait."); - } - Jobs::Execute(gpu_ready_handle); - return; - } - publish_loaded_asset(); - } catch (const std::exception& e) { - promise->set_exception(std::current_exception()); - UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, e.what()); - FinishAssetLoadingImpl(asset_handle); - } catch (...) { - promise->set_exception(std::current_exception()); - UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, "Unknown asset finalization failure."); - FinishAssetLoadingImpl(asset_handle); + ScheduleMainThreadAssetTaskImpl([asset_handle, context = std::move(context), promise, + payload = std::move(payload)]() { + try { + if (!context.asset->ApplyStagedPayloadInternal(context.absolute_path, payload)) { + throw std::runtime_error("Failed to apply staged asset load payload."); + } + context.asset->saved_ = true; + auto publish_loaded_asset = [asset_handle, context, promise]() { + context.file->asset_ = context.asset; + { + auto& asset_manager = GetInstance(); + std::lock_guard lock(asset_manager.asset_registry_.asset_registry_mutex); + asset_manager.asset_registry_.assets_[asset_handle] = context.asset; + } + promise->set_value(context.asset); + UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Loaded, "Asset loaded."); + FinishAssetLoadingImpl(asset_handle); + }; + auto pending_gpu_work = context.asset->ConsumePendingGpuWorkHandles(); + if (!pending_gpu_work.empty()) { + UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::GpuPending, "Waiting for GPU finalization."); + JobOptions gpu_ready_options; + gpu_ready_options.executor = JobExecutorType::Background; + gpu_ready_options.affinity = JobThreadAffinity::Background; + gpu_ready_options.debug_name = "AssetManager::WaitForAssetGpuReady"; + auto pending_handles = std::make_shared>(std::move(pending_gpu_work)); + const auto gpu_ready_handle = Jobs::Run( + *pending_handles, gpu_ready_options, [asset_handle, promise, publish_loaded_asset, pending_handles]() { + try { + for (const auto& handle : *pending_handles) { + Jobs::Wait(handle); + } + ScheduleMainThreadAssetTaskImpl(publish_loaded_asset); + } catch (const std::exception& e) { + promise->set_exception(std::current_exception()); + UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, e.what()); + FinishAssetLoadingImpl(asset_handle); + } catch (...) { + promise->set_exception(std::current_exception()); + UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, + "Unknown asset GPU finalization failure."); + FinishAssetLoadingImpl(asset_handle); + } + }); + if (!gpu_ready_handle.Valid()) { + throw std::runtime_error("Failed to schedule asset GPU readiness wait."); } - }); + Jobs::Execute(gpu_ready_handle); + return; + } + publish_loaded_asset(); + } catch (const std::exception& e) { + promise->set_exception(std::current_exception()); + UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, e.what()); + FinishAssetLoadingImpl(asset_handle); + } catch (...) { + promise->set_exception(std::current_exception()); + UpdateAssetLoadStateImpl(asset_handle, AssetLoadState::Failed, "Unknown asset finalization failure."); + FinishAssetLoadingImpl(asset_handle); + } + }); return; } diff --git a/EvoEngine_SDK/src/GeometryStorage.cpp b/EvoEngine_SDK/src/GeometryStorage.cpp index 55a6b97b..d3540caf 100644 --- a/EvoEngine_SDK/src/GeometryStorage.cpp +++ b/EvoEngine_SDK/src/GeometryStorage.cpp @@ -3,21 +3,59 @@ #include "Jobs.hpp" #include "RenderLayer.hpp" #include "meshoptimizer.h" + +#include +#include +#include + using namespace evo_engine; -namespace { -template -GpuWorkHandle UploadVectorAsync(const std::shared_ptr& buffer, const std::vector& data) { - if (!buffer || data.empty()) { - return {}; +void GeometryStorage::DirtyRange::Mark(const size_t range_begin, const size_t count) { + if (count == 0) { + return; } - return buffer->UploadDataAsync(data.size() * sizeof(T), data.data()); + const auto range_end = range_begin + count; + if (!dirty) { + dirty = true; + begin = range_begin; + end = range_end; + return; + } + begin = std::min(begin, range_begin); + end = std::max(end, range_end); +} + +void GeometryStorage::DirtyRange::MarkTail(const size_t range_begin, const size_t range_end) { + if (range_begin >= range_end) { + return; + } + Mark(range_begin, range_end - range_begin); +} + +void GeometryStorage::DirtyRange::Clear() { + *this = {}; +} + +bool GeometryStorage::DirtyRange::Empty() const { + return !dirty || begin >= end; +} + +void GeometryStorage::ClearMeshDirtyRanges() { + mesh_vertex_dirty_range_.Clear(); + meshlet_dirty_range_.Clear(); + triangle_dirty_range_.Clear(); } -} // namespace -bool GeometryStorage::GeometryUploadSnapshot::Active() const { - return mesh_dirty || mesh_upload_active || skinned_mesh_dirty || skinned_mesh_upload_active || strand_dirty || - strand_upload_active; +void GeometryStorage::ClearSkinnedMeshDirtyRanges() { + skinned_vertex_dirty_range_.Clear(); + skinned_meshlet_dirty_range_.Clear(); + skinned_triangle_dirty_range_.Clear(); +} + +void GeometryStorage::ClearStrandDirtyRanges() { + strand_point_dirty_range_.Clear(); + strand_meshlet_dirty_range_.Clear(); + segment_dirty_range_.Clear(); } void GeometryStorage::CaptureRangeCommits(const std::vector>& descriptors, @@ -68,7 +106,7 @@ bool GeometryStorage::IsPendingUploadCompleted(const PendingGeometryUpload& uplo void GeometryStorage::WaitPendingUpload(PendingGeometryUpload& upload) { auto* gpu_service = Platform::TryGetGpuService(); for (const auto& handle : upload.handles) { - if (!handle.Valid()) { + if (!handle.Valid() || Jobs::IsCompleted(handle)) { continue; } if (gpu_service) { @@ -104,63 +142,84 @@ void GeometryStorage::CompletePendingUploads() { CompletePendingUpload(pending_strand_upload_); } -void GeometryStorage::SchedulePendingUploads() { - if (require_mesh_data_device_update_ && !pending_mesh_upload_.active) { - pending_mesh_upload_.handles = { - UploadVectorAsync(vertex_buffer_, vertex_data_chunks_), - UploadVectorAsync(meshlet_buffer_, meshlets_), - UploadVectorAsync(triangle_buffer_, triangles_), - }; - CaptureRangeCommits(meshlet_range_descriptor_, pending_mesh_upload_.meshlet_commits); - CaptureRangeCommits(triangle_range_descriptor_, pending_mesh_upload_.index_commits); - require_mesh_data_device_update_ = false; - if (HasValidUploadHandle(pending_mesh_upload_)) { - pending_mesh_upload_.active = true; - } else { - ApplyRangeCommits(pending_mesh_upload_.meshlet_commits); - ApplyRangeCommits(pending_mesh_upload_.index_commits); - ClearPendingUpload(pending_mesh_upload_); - version_++; - } +GpuWorkHandle GeometryStorage::ScheduleDirtyBufferUpload(const DirtyBufferUpload& upload) { + if (!upload.buffer || !*upload.buffer || !upload.dirty_range || upload.dirty_range->Empty() || + upload.data == nullptr || upload.element_count == 0 || upload.element_size == 0) { + return {}; } - if (require_skinned_mesh_data_device_update_ && !pending_skinned_mesh_upload_.active) { - pending_skinned_mesh_upload_.handles = { - UploadVectorAsync(skinned_vertex_buffer_, skinned_vertex_data_chunks_), - UploadVectorAsync(skinned_meshlet_buffer_, skinned_meshlets_), - UploadVectorAsync(skinned_triangle_buffer_, skinned_triangles_), - }; - CaptureRangeCommits(skinned_meshlet_range_descriptor_, pending_skinned_mesh_upload_.meshlet_commits); - CaptureRangeCommits(skinned_triangle_range_descriptor_, pending_skinned_mesh_upload_.index_commits); - require_skinned_mesh_data_device_update_ = false; - if (HasValidUploadHandle(pending_skinned_mesh_upload_)) { - pending_skinned_mesh_upload_.active = true; - } else { - ApplyRangeCommits(pending_skinned_mesh_upload_.meshlet_commits); - ApplyRangeCommits(pending_skinned_mesh_upload_.index_commits); - ClearPendingUpload(pending_skinned_mesh_upload_); - version_++; - } + const auto begin = std::min(upload.dirty_range->begin, upload.element_count); + const auto end = std::min(upload.dirty_range->end, upload.element_count); + if (begin >= end) { + return {}; } - if (require_strand_mesh_data_device_update_ && !pending_strand_upload_.active) { - pending_strand_upload_.handles = { - UploadVectorAsync(strand_point_buffer_, strand_point_data_chunks_), - UploadVectorAsync(strand_meshlet_buffer_, strand_meshlets_), - UploadVectorAsync(segment_buffer_, segments_), - }; - CaptureRangeCommits(strand_meshlet_range_descriptor_, pending_strand_upload_.meshlet_commits); - CaptureRangeCommits(segment_range_descriptor_, pending_strand_upload_.index_commits); - require_strand_mesh_data_device_update_ = false; - if (HasValidUploadHandle(pending_strand_upload_)) { - pending_strand_upload_.active = true; - } else { - ApplyRangeCommits(pending_strand_upload_.meshlet_commits); - ApplyRangeCommits(pending_strand_upload_.index_commits); - ClearPendingUpload(pending_strand_upload_); - version_++; + const auto& buffer = *upload.buffer; + const auto full_upload_size = upload.element_count * upload.element_size; + const auto range_offset = begin * upload.element_size; + const auto range_size = (end - begin) * upload.element_size; + if (end * upload.element_size > buffer->GetSize()) { + return buffer->UploadDataAsync(full_upload_size, upload.data); + } + + const auto* bytes = static_cast(upload.data); + return buffer->UploadSubDataAsync(range_size, bytes + range_offset, range_offset); +} + +void GeometryStorage::ScheduleUploadGroup(bool& dirty, PendingGeometryUpload& pending_upload, + std::initializer_list uploads, + const std::vector>& meshlet_descriptors, + const std::vector>& index_descriptors) { + if (!dirty || pending_upload.active) { + return; + } + + pending_upload.handles.clear(); + pending_upload.handles.reserve(uploads.size()); + for (const auto& upload : uploads) { + pending_upload.handles.emplace_back(ScheduleDirtyBufferUpload(upload)); + if (upload.dirty_range) { + upload.dirty_range->Clear(); } } + CaptureRangeCommits(meshlet_descriptors, pending_upload.meshlet_commits); + CaptureRangeCommits(index_descriptors, pending_upload.index_commits); + dirty = false; + + if (HasValidUploadHandle(pending_upload)) { + pending_upload.active = true; + return; + } + + ApplyRangeCommits(pending_upload.meshlet_commits); + ApplyRangeCommits(pending_upload.index_commits); + ClearPendingUpload(pending_upload); + version_++; +} + +void GeometryStorage::SchedulePendingUploads() { + const auto make_upload = [](const std::shared_ptr& buffer, const auto& data, DirtyRange& dirty_range) { + return DirtyBufferUpload{&buffer, data.empty() ? nullptr : static_cast(data.data()), data.size(), + sizeof(typename std::decay_t::value_type), &dirty_range}; + }; + + ScheduleUploadGroup(require_mesh_data_device_update_, pending_mesh_upload_, + {make_upload(vertex_buffer_, vertex_data_chunks_, mesh_vertex_dirty_range_), + make_upload(meshlet_buffer_, meshlets_, meshlet_dirty_range_), + make_upload(triangle_buffer_, triangles_, triangle_dirty_range_)}, + meshlet_range_descriptor_, triangle_range_descriptor_); + + ScheduleUploadGroup(require_skinned_mesh_data_device_update_, pending_skinned_mesh_upload_, + {make_upload(skinned_vertex_buffer_, skinned_vertex_data_chunks_, skinned_vertex_dirty_range_), + make_upload(skinned_meshlet_buffer_, skinned_meshlets_, skinned_meshlet_dirty_range_), + make_upload(skinned_triangle_buffer_, skinned_triangles_, skinned_triangle_dirty_range_)}, + skinned_meshlet_range_descriptor_, skinned_triangle_range_descriptor_); + + ScheduleUploadGroup(require_strand_mesh_data_device_update_, pending_strand_upload_, + {make_upload(strand_point_buffer_, strand_point_data_chunks_, strand_point_dirty_range_), + make_upload(strand_meshlet_buffer_, strand_meshlets_, strand_meshlet_dirty_range_), + make_upload(segment_buffer_, segments_, segment_dirty_range_)}, + strand_meshlet_range_descriptor_, segment_range_descriptor_); } void GeometryStorage::UploadData() { @@ -222,6 +281,7 @@ void GeometryStorage::Initialize() { storage.triangle_buffer_ = std::make_shared(storage_buffer_create_info, vertices_vma_allocation_create_info); storage.require_mesh_data_device_update_ = false; + storage.ClearMeshDirtyRanges(); storage_buffer_create_info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; @@ -237,6 +297,7 @@ void GeometryStorage::Initialize() { std::make_shared(storage_buffer_create_info, vertices_vma_allocation_create_info); storage.require_skinned_mesh_data_device_update_ = false; + storage.ClearSkinnedMeshDirtyRanges(); storage_buffer_create_info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; @@ -251,6 +312,7 @@ void GeometryStorage::Initialize() { storage.segment_buffer_ = std::make_shared(storage_buffer_create_info, vertices_vma_allocation_create_info); storage.require_strand_mesh_data_device_update_ = false; + storage.ClearStrandDirtyRanges(); storage.initialized_ = true; } @@ -258,28 +320,11 @@ uint32_t GeometryStorage::GetVersion() { return GetInstance().version_; } -GeometryStorage::GeometryUploadSnapshot GeometryStorage::GetUploadSnapshot() { - const auto& storage = GetInstance(); - GeometryUploadSnapshot snapshot; - snapshot.mesh_dirty = storage.require_mesh_data_device_update_; - snapshot.mesh_upload_active = storage.pending_mesh_upload_.active; - snapshot.mesh_upload_completed = IsPendingUploadCompleted(storage.pending_mesh_upload_); - snapshot.mesh_upload_handles = storage.pending_mesh_upload_.handles.size(); - - snapshot.skinned_mesh_dirty = storage.require_skinned_mesh_data_device_update_; - snapshot.skinned_mesh_upload_active = storage.pending_skinned_mesh_upload_.active; - snapshot.skinned_mesh_upload_completed = IsPendingUploadCompleted(storage.pending_skinned_mesh_upload_); - snapshot.skinned_mesh_upload_handles = storage.pending_skinned_mesh_upload_.handles.size(); - - snapshot.strand_dirty = storage.require_strand_mesh_data_device_update_; - snapshot.strand_upload_active = storage.pending_strand_upload_.active; - snapshot.strand_upload_completed = IsPendingUploadCompleted(storage.pending_strand_upload_); - snapshot.strand_upload_handles = storage.pending_strand_upload_.handles.size(); - return snapshot; -} - bool GeometryStorage::HasPendingUploads() { - return GetUploadSnapshot().Active(); + const auto& storage = GetInstance(); + return storage.require_mesh_data_device_update_ || storage.pending_mesh_upload_.active || + storage.require_skinned_mesh_data_device_update_ || storage.pending_skinned_mesh_upload_.active || + storage.require_strand_mesh_data_device_update_ || storage.pending_strand_upload_.active; } void GeometryStorage::WaitForPendingUploads() { @@ -376,15 +421,17 @@ void GeometryStorage::AllocateMesh(const Handle& handle, std::vector& ve auto& storage = GetInstance(); WaitPendingUpload(storage.pending_mesh_upload_); storage.CompletePendingUpload(storage.pending_mesh_upload_); + const auto meshlet_begin = storage.meshlets_.size(); + const auto triangle_begin = storage.triangles_.size(); // const auto meshletRange = std::make_shared(); target_meshlet_range->handle_ = handle; - target_meshlet_range->offset = storage.meshlets_.size(); + target_meshlet_range->offset = meshlet_begin; target_meshlet_range->range = 0; // const auto triangleRange = std::make_shared(); target_triangle_range->handle_ = handle; - target_triangle_range->offset = storage.triangles_.size(); + target_triangle_range->offset = triangle_begin; target_triangle_range->range = 0; target_triangle_range->index_count = triangles.size(); @@ -449,6 +496,9 @@ void GeometryStorage::AllocateMesh(const Handle& handle, std::vector& ve storage.meshlet_range_descriptor_.push_back(target_meshlet_range); storage.triangle_range_descriptor_.push_back(target_triangle_range); + storage.mesh_vertex_dirty_range_.Mark(meshlet_begin, target_meshlet_range->range); + storage.meshlet_dirty_range_.Mark(meshlet_begin, target_meshlet_range->range); + storage.triangle_dirty_range_.Mark(triangle_begin, target_triangle_range->range); storage.require_mesh_data_device_update_ = true; } @@ -462,13 +512,15 @@ void GeometryStorage::AllocateSkinnedMesh(const Handle& handle, const std::vecto auto& storage = GetInstance(); WaitPendingUpload(storage.pending_skinned_mesh_upload_); storage.CompletePendingUpload(storage.pending_skinned_mesh_upload_); + const auto skinned_meshlet_begin = storage.skinned_meshlets_.size(); + const auto skinned_triangle_begin = storage.skinned_triangles_.size(); target_skinned_meshlet_range->handle_ = handle; - target_skinned_meshlet_range->offset = storage.skinned_meshlets_.size(); + target_skinned_meshlet_range->offset = skinned_meshlet_begin; target_skinned_meshlet_range->range = 0; target_skinned_triangle_range->handle_ = handle; - target_skinned_triangle_range->offset = storage.skinned_triangles_.size(); + target_skinned_triangle_range->offset = skinned_triangle_begin; target_skinned_triangle_range->range = 0; target_skinned_triangle_range->index_count = skinned_triangles.size(); std::vector skinned_meshlets_results{}; @@ -522,6 +574,9 @@ void GeometryStorage::AllocateSkinnedMesh(const Handle& handle, const std::vecto } storage.skinned_meshlet_range_descriptor_.push_back(target_skinned_meshlet_range); storage.skinned_triangle_range_descriptor_.push_back(target_skinned_triangle_range); + storage.skinned_vertex_dirty_range_.Mark(skinned_meshlet_begin, target_skinned_meshlet_range->range); + storage.skinned_meshlet_dirty_range_.Mark(skinned_meshlet_begin, target_skinned_meshlet_range->range); + storage.skinned_triangle_dirty_range_.Mark(skinned_triangle_begin, target_skinned_triangle_range->range); storage.require_skinned_mesh_data_device_update_ = true; } @@ -535,14 +590,16 @@ void GeometryStorage::AllocateStrands(const Handle& handle, const std::vectorhandle_ = handle; - target_strand_meshlet_range->offset = storage.strand_meshlets_.size(); + target_strand_meshlet_range->offset = strand_meshlet_begin; target_strand_meshlet_range->range = 0; target_segment_range->handle_ = handle; - target_segment_range->offset = storage.segments_.size(); + target_segment_range->offset = segment_begin; target_segment_range->range = 0; target_segment_range->index_count = segments.size(); @@ -653,6 +710,9 @@ void GeometryStorage::AllocateStrands(const Handle& handle, const std::vectorrange); + storage.strand_meshlet_dirty_range_.Mark(strand_meshlet_begin, target_strand_meshlet_range->range); + storage.segment_dirty_range_.Mark(segment_begin, target_segment_range->range); storage.require_strand_mesh_data_device_update_ = true; } @@ -674,13 +734,14 @@ void GeometryStorage::FreeMesh(const Handle& handle) { return; } const auto& meshlet_range_descriptor = storage.meshlet_range_descriptor_[meshlet_range_descriptor_index]; + const auto meshlet_remove_offset = meshlet_range_descriptor->offset; const uint32_t remove_chunk_size = meshlet_range_descriptor->range; storage.meshlets_.erase(storage.meshlets_.begin() + meshlet_range_descriptor->offset, storage.meshlets_.begin() + meshlet_range_descriptor->offset + remove_chunk_size); storage.vertex_data_chunks_.erase( storage.vertex_data_chunks_.begin() + meshlet_range_descriptor->offset, storage.vertex_data_chunks_.begin() + meshlet_range_descriptor->offset + remove_chunk_size); - for (uint32_t i = meshlet_range_descriptor_index; i < storage.meshlets_.size(); i++) { + for (uint32_t i = meshlet_remove_offset; i < storage.meshlets_.size(); i++) { storage.meshlets_[i].vertex_chunk_index = i; } for (uint32_t i = meshlet_range_descriptor_index + 1; i < storage.meshlet_range_descriptor_.size(); i++) { @@ -688,6 +749,8 @@ void GeometryStorage::FreeMesh(const Handle& handle) { storage.meshlet_range_descriptor_[i]->offset -= meshlet_range_descriptor->range; } storage.meshlet_range_descriptor_.erase(storage.meshlet_range_descriptor_.begin() + meshlet_range_descriptor_index); + storage.mesh_vertex_dirty_range_.MarkTail(meshlet_remove_offset, storage.vertex_data_chunks_.size()); + storage.meshlet_dirty_range_.MarkTail(meshlet_remove_offset, storage.meshlets_.size()); uint32_t triangle_range_descriptor_index = UINT_MAX; for (uint32_t i = 0; i < storage.triangle_range_descriptor_.size(); i++) { @@ -700,6 +763,7 @@ void GeometryStorage::FreeMesh(const Handle& handle) { return; } const auto& triangle_range_descriptor = storage.triangle_range_descriptor_[triangle_range_descriptor_index]; + const auto triangle_remove_offset = triangle_range_descriptor->offset; storage.triangles_.erase( storage.triangles_.begin() + triangle_range_descriptor->offset, storage.triangles_.begin() + triangle_range_descriptor->offset + triangle_range_descriptor->range); @@ -715,6 +779,7 @@ void GeometryStorage::FreeMesh(const Handle& handle) { } storage.triangle_range_descriptor_.erase(storage.triangle_range_descriptor_.begin() + triangle_range_descriptor_index); + storage.triangle_dirty_range_.MarkTail(triangle_remove_offset, storage.triangles_.size()); storage.require_mesh_data_device_update_ = true; } @@ -737,6 +802,7 @@ void GeometryStorage::FreeSkinnedMesh(const Handle& handle) { }; const auto& skinned_meshlet_range_descriptor = storage.skinned_meshlet_range_descriptor_[skinned_meshlet_range_descriptor_index]; + const auto skinned_meshlet_remove_offset = skinned_meshlet_range_descriptor->offset; const uint32_t remove_chunk_size = skinned_meshlet_range_descriptor->range; storage.skinned_meshlets_.erase( storage.skinned_meshlets_.begin() + skinned_meshlet_range_descriptor->offset, @@ -744,7 +810,7 @@ void GeometryStorage::FreeSkinnedMesh(const Handle& handle) { storage.skinned_vertex_data_chunks_.erase( storage.skinned_vertex_data_chunks_.begin() + skinned_meshlet_range_descriptor->offset, storage.skinned_vertex_data_chunks_.begin() + skinned_meshlet_range_descriptor->offset + remove_chunk_size); - for (uint32_t i = skinned_meshlet_range_descriptor_index; i < storage.skinned_meshlets_.size(); i++) { + for (uint32_t i = skinned_meshlet_remove_offset; i < storage.skinned_meshlets_.size(); i++) { storage.skinned_meshlets_[i].skinned_vertex_chunk_index = i; } for (uint32_t i = skinned_meshlet_range_descriptor_index + 1; i < storage.skinned_meshlet_range_descriptor_.size(); @@ -754,6 +820,9 @@ void GeometryStorage::FreeSkinnedMesh(const Handle& handle) { } storage.skinned_meshlet_range_descriptor_.erase(storage.skinned_meshlet_range_descriptor_.begin() + skinned_meshlet_range_descriptor_index); + storage.skinned_vertex_dirty_range_.MarkTail(skinned_meshlet_remove_offset, + storage.skinned_vertex_data_chunks_.size()); + storage.skinned_meshlet_dirty_range_.MarkTail(skinned_meshlet_remove_offset, storage.skinned_meshlets_.size()); uint32_t skinned_triangle_range_descriptor_index = UINT_MAX; for (uint32_t i = 0; i < storage.skinned_triangle_range_descriptor_.size(); i++) { @@ -767,6 +836,7 @@ void GeometryStorage::FreeSkinnedMesh(const Handle& handle) { } const auto& skinned_triangle_range_descriptor = storage.skinned_triangle_range_descriptor_[skinned_triangle_range_descriptor_index]; + const auto skinned_triangle_remove_offset = skinned_triangle_range_descriptor->offset; storage.skinned_triangles_.erase(storage.skinned_triangles_.begin() + skinned_triangle_range_descriptor->offset, storage.skinned_triangles_.begin() + skinned_triangle_range_descriptor->offset + skinned_triangle_range_descriptor->range); @@ -784,6 +854,7 @@ void GeometryStorage::FreeSkinnedMesh(const Handle& handle) { storage.skinned_triangle_range_descriptor_.erase(storage.skinned_triangle_range_descriptor_.begin() + skinned_triangle_range_descriptor_index); + storage.skinned_triangle_dirty_range_.MarkTail(skinned_triangle_remove_offset, storage.skinned_triangles_.size()); storage.require_skinned_mesh_data_device_update_ = true; } @@ -806,6 +877,7 @@ void GeometryStorage::FreeStrands(const Handle& handle) { } const auto& strand_meshlet_range_descriptor = storage.strand_meshlet_range_descriptor_[strand_meshlet_range_descriptor_index]; + const auto strand_meshlet_remove_offset = strand_meshlet_range_descriptor->offset; const uint32_t remove_chunk_size = strand_meshlet_range_descriptor->range; storage.strand_meshlets_.erase( storage.strand_meshlets_.begin() + strand_meshlet_range_descriptor->offset, @@ -813,7 +885,7 @@ void GeometryStorage::FreeStrands(const Handle& handle) { storage.strand_point_data_chunks_.erase( storage.strand_point_data_chunks_.begin() + strand_meshlet_range_descriptor->offset, storage.strand_point_data_chunks_.begin() + strand_meshlet_range_descriptor->offset + remove_chunk_size); - for (uint32_t i = strand_meshlet_range_descriptor_index; i < storage.strand_meshlets_.size(); i++) { + for (uint32_t i = strand_meshlet_remove_offset; i < storage.strand_meshlets_.size(); i++) { storage.strand_meshlets_[i].strand_point_chunk_index = i; } for (uint32_t i = strand_meshlet_range_descriptor_index + 1; i < storage.strand_meshlet_range_descriptor_.size(); @@ -823,6 +895,8 @@ void GeometryStorage::FreeStrands(const Handle& handle) { } storage.strand_meshlet_range_descriptor_.erase(storage.strand_meshlet_range_descriptor_.begin() + strand_meshlet_range_descriptor_index); + storage.strand_point_dirty_range_.MarkTail(strand_meshlet_remove_offset, storage.strand_point_data_chunks_.size()); + storage.strand_meshlet_dirty_range_.MarkTail(strand_meshlet_remove_offset, storage.strand_meshlets_.size()); uint32_t segment_range_descriptor_index = UINT_MAX; for (uint32_t i = 0; i < storage.segment_range_descriptor_.size(); i++) { @@ -835,6 +909,7 @@ void GeometryStorage::FreeStrands(const Handle& handle) { return; } const auto& segment_range_descriptor = storage.segment_range_descriptor_[segment_range_descriptor_index]; + const auto segment_remove_offset = segment_range_descriptor->offset; storage.segments_.erase( storage.segments_.begin() + segment_range_descriptor->offset, storage.segments_.begin() + segment_range_descriptor->offset + segment_range_descriptor->range); @@ -850,6 +925,7 @@ void GeometryStorage::FreeStrands(const Handle& handle) { storage.segments_[i].w -= remove_chunk_size * Platform::Constants::meshlet_max_vertices_size; } storage.segment_range_descriptor_.erase(storage.segment_range_descriptor_.begin() + segment_range_descriptor_index); + storage.segment_dirty_range_.MarkTail(segment_remove_offset, storage.segments_.size()); storage.require_strand_mesh_data_device_update_ = true; } @@ -937,6 +1013,9 @@ void GeometryStorage::OnDestroy() { ClearPendingUpload(storage.pending_mesh_upload_); ClearPendingUpload(storage.pending_skinned_mesh_upload_); ClearPendingUpload(storage.pending_strand_upload_); + storage.ClearMeshDirtyRanges(); + storage.ClearSkinnedMeshDirtyRanges(); + storage.ClearStrandDirtyRanges(); storage.vertex_data_chunks_.clear(); storage.meshlets_.clear(); diff --git a/EvoEngine_SDK/src/GraphicsResources.cpp b/EvoEngine_SDK/src/GraphicsResources.cpp index a3b98fdb..807f42a3 100644 --- a/EvoEngine_SDK/src/GraphicsResources.cpp +++ b/EvoEngine_SDK/src/GraphicsResources.cpp @@ -549,14 +549,19 @@ struct Buffer::GpuState { mutable std::vector pending_gpu_work; }; -void Buffer::UploadDataOnGpuThread(const std::shared_ptr& state, const size_t size, const void* src) { - if (size > state->size) - ResizeOnGpuThread(state, size); +void Buffer::UploadDataOnGpuThread(const std::shared_ptr& state, const size_t size, const void* src, + const VkDeviceSize dst_offset) { + const auto required_size = dst_offset + size; + if (required_size > state->size && dst_offset != 0) { + throw std::runtime_error("Subrange buffer upload cannot grow the destination buffer."); + } + if (required_size > state->size) + ResizeOnGpuThread(state, required_size); if (state->vma_allocation_create_info.flags & VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT || state->vma_allocation_create_info.flags & VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT) { void* mapping; Platform::CheckVk(vmaMapMemory(Platform::GetVmaAllocator(), state->vma_allocation, &mapping)); - memcpy(mapping, src, size); + memcpy(static_cast(mapping) + dst_offset, src, size); vmaUnmapMemory(Platform::GetVmaAllocator(), state->vma_allocation); } else { auto& gpu_service = Platform::GetGpuService(); @@ -566,7 +571,7 @@ void Buffer::UploadDataOnGpuThread(const std::shared_ptr& state, const Platform::CheckVk(vmaMapMemory(Platform::GetVmaAllocator(), staging_buffer.vma_allocation, &mapping)); memcpy(mapping, src, size); vmaUnmapMemory(Platform::GetVmaAllocator(), staging_buffer.vma_allocation); - CopyFromBufferOnGpuThread(state, staging_buffer.vk_buffer, size, 0, 0); + CopyFromBufferOnGpuThread(state, staging_buffer.vk_buffer, size, 0, dst_offset); gpu_service.ReleaseStagingBuffer(staging_buffer); } catch (...) { gpu_service.ReleaseStagingBuffer(staging_buffer); @@ -580,7 +585,16 @@ void Buffer::UploadData(const size_t size, const void* src) { Platform::GetGpuService().Wait(handle); } +void Buffer::UploadSubData(const size_t size, const void* src, const VkDeviceSize dst_offset) { + const auto handle = UploadSubDataAsync(size, src, dst_offset); + Platform::GetGpuService().Wait(handle); +} + GpuWorkHandle Buffer::UploadDataAsync(const size_t size, const void* src) { + return UploadSubDataAsync(size, src, 0); +} + +GpuWorkHandle Buffer::UploadSubDataAsync(const size_t size, const void* src, const VkDeviceSize dst_offset) { if (size == 0) { return {}; } @@ -591,11 +605,11 @@ GpuWorkHandle Buffer::UploadDataAsync(const size_t size, const void* src) { memcpy(owned_data->data(), src, size); GpuWorkOptions options; - options.debug_name = "Buffer::UploadDataAsync"; + options.debug_name = dst_offset == 0 ? "Buffer::UploadDataAsync" : "Buffer::UploadSubDataAsync"; const auto state = gpu_state_; auto& gpu_service = Platform::GetGpuService(); - const auto handle = gpu_service.EnqueueStaging(size, options, [state, size, owned_data]() { - UploadDataOnGpuThread(state, size, owned_data->data()); + const auto handle = gpu_service.EnqueueStaging(size, options, [state, size, owned_data, dst_offset]() { + UploadDataOnGpuThread(state, size, owned_data->data(), dst_offset); }); TrackPendingGpuWork(handle); return handle; @@ -854,7 +868,13 @@ void Buffer::CopyFromBufferOnGpuThread(const std::shared_ptr& state, void Buffer::CopyFromBufferOnGpuThread(const std::shared_ptr& state, const VkBuffer src_buffer, const VkDeviceSize size, const VkDeviceSize src_offset, const VkDeviceSize dst_offset) { - ResizeOnGpuThread(state, size); + const auto required_size = dst_offset + size; + if (required_size > state->size && dst_offset != 0) { + throw std::runtime_error("Subrange buffer copy cannot grow the destination buffer."); + } + if (required_size > state->size) { + ResizeOnGpuThread(state, required_size); + } Platform::GetGpuService().SubmitImmediate([&](const VkCommandBuffer vk_command_buffer) { VkBufferCopy copy_region{}; copy_region.size = size; @@ -1010,6 +1030,10 @@ const VkBuffer& Buffer::GetVkBuffer() const { return gpu_state_->vk_buffer; } +VkDeviceSize Buffer::GetSize() const { + return gpu_state_->size; +} + VmaAllocation Buffer::GetVmaAllocation() const { return gpu_state_->vma_allocation; } diff --git a/EvoEngine_SDK/src/IAsset.cpp b/EvoEngine_SDK/src/IAsset.cpp index 1bb71b81..10437831 100644 --- a/EvoEngine_SDK/src/IAsset.cpp +++ b/EvoEngine_SDK/src/IAsset.cpp @@ -125,20 +125,7 @@ void IAsset::TrackPendingGpuWork(const JobHandle &handle) { pending_gpu_work_state_->handles.emplace_back(handle); } -bool IAsset::HasPendingGpuWork() const { - if (!pending_gpu_work_state_) { - return false; - } - std::lock_guard lock(pending_gpu_work_state_->mutex); - for (const auto &handle : pending_gpu_work_state_->handles) { - if (handle.Valid()) { - return true; - } - } - return false; -} - -std::vector IAsset::GetPendingGpuWorkHandles() const { +std::vector IAsset::ConsumePendingGpuWorkHandles() const { if (!pending_gpu_work_state_) { return {}; } @@ -150,19 +137,15 @@ std::vector IAsset::GetPendingGpuWorkHandles() const { handles.emplace_back(handle); } } + pending_gpu_work_state_->handles.clear(); return handles; } void IAsset::WaitForPendingGpuWork() const { - const auto handles = GetPendingGpuWorkHandles(); + const auto handles = ConsumePendingGpuWorkHandles(); for (const auto &handle : handles) { Jobs::Wait(handle); } - if (!pending_gpu_work_state_) { - return; - } - std::lock_guard lock(pending_gpu_work_state_->mutex); - pending_gpu_work_state_->handles.clear(); } void IAsset::OnCreate() { diff --git a/EvoEngine_SDK/src/Platform.cpp b/EvoEngine_SDK/src/Platform.cpp index 48da0ee3..c7956192 100644 --- a/EvoEngine_SDK/src/Platform.cpp +++ b/EvoEngine_SDK/src/Platform.cpp @@ -405,6 +405,19 @@ void Platform::WaitForDeviceIdle() { CheckVk(vkDeviceWaitIdle(graphics.vk_device_)); } +void Platform::DrainGpuResourceWork() { + if (!Initialized()) { + return; + } + GeometryStorage::WaitForPendingUploads(); + TextureStorage::DeviceSync(); + if (const auto gpu_service = TryGetGpuService(); + gpu_service && gpu_service->GetLifecycleState() == GpuService::LifecycleState::Running) { + gpu_service->WaitIdle(); + } + WaitForDeviceIdle(); +} + void Platform::TransitImageLayout(VkCommandBuffer vk_command_buffer, const VkImage target_image, const VkFormat image_format, const uint32_t layer_count, const VkImageLayout old_layout, const VkImageLayout new_layout, diff --git a/EvoEngine_SDK/src/RenderLayer.cpp b/EvoEngine_SDK/src/RenderLayer.cpp index 0f7604c7..69c13993 100644 --- a/EvoEngine_SDK/src/RenderLayer.cpp +++ b/EvoEngine_SDK/src/RenderLayer.cpp @@ -2222,16 +2222,7 @@ void RenderLayer::PreUpdate() { } void RenderLayer::OnDestroy() { - if (!Platform::Initialized()) { - return; - } - GeometryStorage::WaitForPendingUploads(); - TextureStorage::DeviceSync(); - if (const auto gpu_service = Platform::TryGetGpuService(); - gpu_service && gpu_service->GetLifecycleState() == GpuService::LifecycleState::Running) { - gpu_service->WaitIdle(); - } - Platform::WaitForDeviceIdle(); + Platform::DrainGpuResourceWork(); } uint32_t RenderLayer::DrawMesh(const std::shared_ptr& mesh, const std::shared_ptr& material, diff --git a/EvoEngine_SDK/src/Resources.cpp b/EvoEngine_SDK/src/Resources.cpp index 6694e6fe..5fe81d00 100644 --- a/EvoEngine_SDK/src/Resources.cpp +++ b/EvoEngine_SDK/src/Resources.cpp @@ -246,15 +246,7 @@ bool Resources::IsResource(const AssetRef& target) { void Resources::OnDestroy() { auto& resources = GetInstance(); - if (Platform::Initialized()) { - GeometryStorage::WaitForPendingUploads(); - TextureStorage::DeviceSync(); - if (const auto gpu_service = Platform::TryGetGpuService(); - gpu_service && gpu_service->GetLifecycleState() == GpuService::LifecycleState::Running) { - gpu_service->WaitIdle(); - } - Platform::WaitForDeviceIdle(); - } + Platform::DrainGpuResourceWork(); resources.typed_resources_.clear(); resources.resources_.clear(); diff --git a/EvoEngine_SDK/src/Texture2D.cpp b/EvoEngine_SDK/src/Texture2D.cpp index 35a9415b..acc79d00 100644 --- a/EvoEngine_SDK/src/Texture2D.cpp +++ b/EvoEngine_SDK/src/Texture2D.cpp @@ -103,7 +103,7 @@ void Texture2D::DownloadData() { void Texture2D::UnsafeUploadDataImmediately() const { auto& texture_storage = TextureStorage::RefTexture2DStorage(texture_storage_handle_); - texture_storage.UploadDataImmediately(); + texture_storage.UploadPendingDataImmediately(); WaitForPendingGpuWork(); } bool Texture2D::SaveInternal(const std::filesystem::path& path) const { @@ -181,7 +181,10 @@ bool Texture2D::LoadInternal(const std::filesystem::path& path) { local_data_.resize(width * height); memcpy(local_data_.data(), data, sizeof(glm::vec4) * width * height); auto& texture_storage = TextureStorage::RefTexture2DStorage(texture_storage_handle_); - texture_storage.SetDataImmediately(local_data_, {width, height}); + const auto upload = texture_storage.SetDataAsync(local_data_, {width, height}); + if (upload.Valid()) { + Platform::GetGpuService().Wait(upload); + } } else { EVOENGINE_ERROR("Texture failed to load at path: " + path.filename().string()); return false; diff --git a/EvoEngine_SDK/src/TextureStorage.cpp b/EvoEngine_SDK/src/TextureStorage.cpp index 06c6f859..5b77016e 100644 --- a/EvoEngine_SDK/src/TextureStorage.cpp +++ b/EvoEngine_SDK/src/TextureStorage.cpp @@ -260,10 +260,6 @@ void Texture2DStorage::Initialize(const glm::uvec2& resolution) { image->GetLayout()); } -void Texture2DStorage::SetDataImmediately(const std::vector& data, const glm::uvec2& resolution) { - UploadData(data, resolution); -} - GpuWorkHandle Texture2DStorage::SetDataAsync(const std::vector& data, const glm::uvec2& resolution) { if (!Platform::Initialized() || data.empty() || resolution.x == 0 || resolution.y == 0) { return {}; @@ -309,69 +305,6 @@ GpuWorkHandle Texture2DStorage::SetDataAsync(const std::vector& data, } } -void Texture2DStorage::UploadData(const std::vector& data, const glm::uvec2& resolution) { - if (!Platform::Initialized()) - return; - Initialize(resolution); - auto image_size = resolution.x * resolution.y; - switch (Platform::Constants::texture_2d) { - case VK_FORMAT_R32G32B32A32_SFLOAT: { - image_size *= sizeof(glm::vec4); - break; - } - case VK_FORMAT_R16G16B16A16_SFLOAT: { - image_size *= 4 * sizeof(glm::detail::hdata); - break; - } - } - Buffer staging_buffer(image_size, false); - switch (Platform::Constants::texture_2d) { - case VK_FORMAT_R32G32B32A32_SFLOAT: { - staging_buffer.UploadVector(data); - break; - } - case VK_FORMAT_R16G16B16A16_SFLOAT: { - std::vector half_size_data(4 * data.size()); - Jobs::RunParallelFor(resolution.x * resolution.y, [&](const auto i) { - half_size_data[i * 4] = glm::detail::toFloat16(data[i][0]); - half_size_data[i * 4 + 1] = glm::detail::toFloat16(data[i][1]); - half_size_data[i * 4 + 2] = glm::detail::toFloat16(data[i][2]); - half_size_data[i * 4 + 3] = glm::detail::toFloat16(data[i][3]); - }); - staging_buffer.UploadVector(half_size_data); - break; - } - } - /* - staging_buffer_create_info.size = image_size; - const Buffer staging_buffer{staging_buffer_create_info, staging_buffer_vma_allocation_create_info}; - void* device_data = nullptr; - vmaMapMemory(Platform::GetVmaAllocator(), staging_buffer.GetVmaAllocation(), &device_data); - switch (Platform::Constants::texture_2d) { - case VK_FORMAT_R32G32B32A32_SFLOAT: { - memcpy(device_data, data.data(), image_size); - break; - } - case VK_FORMAT_R16G16B16A16_SFLOAT: { - std::vector half_size_data(4 * data.size()); - Jobs::RunParallelFor(resolution.x * resolution.y, [&](const auto i) { - half_size_data[i * 4] = glm::detail::toFloat16(data[i][0]); - half_size_data[i * 4 + 1] = glm::detail::toFloat16(data[i][1]); - half_size_data[i * 4 + 2] = glm::detail::toFloat16(data[i][2]); - half_size_data[i * 4 + 3] = glm::detail::toFloat16(data[i][3]); - }); - memcpy(device_data, half_size_data.data(), image_size); - break; - } - }*/ - Platform::ImmediateSubmit([&](const VkCommandBuffer vk_command_buffer) { - image->TransitImageLayout(vk_command_buffer, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - image->CopyFromBuffer(vk_command_buffer, staging_buffer.GetVkBuffer()); - image->GenerateMipmaps(vk_command_buffer); - image->TransitImageLayout(vk_command_buffer, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - }); -} - void Texture2DStorage::Clear() { if (!Platform::Initialized()) return; @@ -403,7 +336,7 @@ void Texture2DStorage::SetData(const std::vector& data, const glm::uv new_resolution_ = resolution; } -void Texture2DStorage::UploadDataImmediately() { +void Texture2DStorage::UploadPendingDataImmediately() { if (new_data_.empty()) { return; } @@ -461,17 +394,6 @@ void TextureStorage::DeviceSync() { texture_index--; } } - - for (int texture_index = 0; texture_index < storage.cubemaps_.size(); texture_index++) { - auto& texture_storage = storage.cubemaps_[texture_index]; - /* - if (!texture_storage.new_data_.empty()) { - texture_storage.UploadData(texture_storage.new_data_, texture_storage.new_resolution_); - texture_storage.new_data_.clear(); - texture_storage.new_resolution_ = {}; - } - */ - } } void TextureStorage::BindTexture2DToDescriptorSet(const std::shared_ptr& descriptor_set, diff --git a/EvoEngine_Tests/Core/GpuServiceTest.cpp b/EvoEngine_Tests/Core/GpuServiceTest.cpp index 03f50a51..a7f7b0c0 100644 --- a/EvoEngine_Tests/Core/GpuServiceTest.cpp +++ b/EvoEngine_Tests/Core/GpuServiceTest.cpp @@ -263,6 +263,39 @@ TEST(GpuService, BufferUploadReadbackRoundTrip) { } } +TEST(GpuService, BufferUploadSubrangeRoundTrip) { + ScopedGpuPlatform platform; + auto& gpu_service = Platform::GetGpuService(); + + constexpr std::array input = {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}; + constexpr std::array patch = {42u, 43u}; + constexpr auto byte_size = static_cast(input.size() * sizeof(input[0])); + constexpr auto patch_offset = static_cast(3 * sizeof(input[0])); + + VkBufferCreateInfo buffer_create_info{}; + buffer_create_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + buffer_create_info.size = byte_size; + buffer_create_info.usage = + VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + buffer_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + VmaAllocationCreateInfo allocation_create_info{}; + allocation_create_info.usage = VMA_MEMORY_USAGE_AUTO; + + Buffer buffer(buffer_create_info, allocation_create_info); + gpu_service.Wait(buffer.UploadDataAsync(static_cast(byte_size), input.data())); + gpu_service.Wait(buffer.UploadSubDataAsync(patch.size() * sizeof(patch[0]), patch.data(), patch_offset)); + + const auto downloaded_bytes = buffer.DownloadDataAsync(static_cast(byte_size)).get(); + std::array output{}; + memcpy(output.data(), downloaded_bytes.data(), downloaded_bytes.size()); + + auto expected = input; + expected[3] = patch[0]; + expected[4] = patch[1]; + EXPECT_EQ(output, expected); +} + TEST(GpuService, Texture2DAsyncUploadProducesReadyImage) { ScopedGpuPlatform platform; auto& gpu_service = Platform::GetGpuService(); @@ -301,10 +334,9 @@ TEST(GpuService, Texture2DRuntimeUpdateTracksGpuReadiness) { auto& texture_storage = texture.RefTexture2DStorage(); texture.SetRgbaChannelData(pixels, glm::uvec2(2, 2)); - EXPECT_TRUE(texture.HasPendingGpuWork()); - texture.WaitForPendingGpuWork(); + EXPECT_TRUE(texture_storage.IsGpuUploadPending()); + Platform::GetGpuService().WaitIdle(); - EXPECT_FALSE(texture.HasPendingGpuWork()); EXPECT_FALSE(texture_storage.IsGpuUploadPending()); EXPECT_EQ(texture_storage.GetLayout(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); @@ -377,6 +409,51 @@ TEST(GpuService, GeometryStorageCommitsMeshRangesAfterAsyncUpload) { EXPECT_EQ(triangle_range->prev_frame_offset, triangle_range->offset); } +TEST(GpuService, GeometryStorageUploadsCompactedMeshTail) { + ScopedGpuPlatform platform; + auto& gpu_service = Platform::GetGpuService(); + + VertexAttributes attributes{}; + attributes.normal = true; + attributes.tangent = true; + const std::vector triangles = {glm::uvec3(0, 1, 2)}; + + auto first_mesh = std::make_unique(); + first_mesh->OnCreate(); + std::vector first_vertices(3); + first_vertices[0].position = glm::vec3(1.0f, 0.0f, 0.0f); + first_vertices[1].position = glm::vec3(2.0f, 0.0f, 0.0f); + first_vertices[2].position = glm::vec3(1.0f, 1.0f, 0.0f); + first_mesh->SetVertices(attributes, first_vertices, triangles); + + auto second_mesh = std::make_unique(); + second_mesh->OnCreate(); + std::vector second_vertices(3); + second_vertices[0].position = glm::vec3(10.0f, 0.0f, 0.0f); + second_vertices[1].position = glm::vec3(11.0f, 0.0f, 0.0f); + second_vertices[2].position = glm::vec3(10.0f, 1.0f, 0.0f); + second_mesh->SetVertices(attributes, second_vertices, triangles); + + PlatformLifecycleTestAccess::PreUpdate(); + gpu_service.WaitIdle(); + PlatformLifecycleTestAccess::PreUpdate(); + ASSERT_EQ(second_mesh->GetTriangleRange()->prev_frame_offset, 1u); + + first_mesh.reset(); + PlatformLifecycleTestAccess::PreUpdate(); + gpu_service.WaitIdle(); + PlatformLifecycleTestAccess::PreUpdate(); + + ASSERT_EQ(second_mesh->GetTriangleRange()->prev_frame_offset, 0u); + + const auto downloaded_bytes = GeometryStorage::GetVertexBuffer()->DownloadDataAsync(sizeof(VertexDataChunk)).get(); + VertexDataChunk output{}; + memcpy(&output, downloaded_bytes.data(), downloaded_bytes.size()); + EXPECT_FLOAT_EQ(output.vertex_data[0].position.x, second_vertices[0].position.x); + EXPECT_FLOAT_EQ(output.vertex_data[0].position.y, second_vertices[0].position.y); + EXPECT_FLOAT_EQ(output.vertex_data[0].position.z, second_vertices[0].position.z); +} + TEST(GpuService, ConcurrentBufferUploadsRoundTrip) { ScopedGpuPlatform platform; auto& gpu_service = Platform::GetGpuService(); diff --git a/PythonBinding/src/PyEvoEngine.cpp b/PythonBinding/src/PyEvoEngine.cpp index 1a29db72..babab797 100644 --- a/PythonBinding/src/PyEvoEngine.cpp +++ b/PythonBinding/src/PyEvoEngine.cpp @@ -63,24 +63,11 @@ bool PyEvoEngine::CaptureCurrentScene(const int resolution_x, const int resoluti } if (!is_scene_ready()) { const auto snapshot = AssetManager::GetAssetLoadSnapshot(); - const auto geometry_snapshot = GeometryStorage::GetUploadSnapshot(); EVOENGINE_ERROR("Scene is not ready for capture! Frames: " + std::to_string(readiness_frames) + ", project idle: " + std::to_string(ProjectManager::IsProjectIdle()) + ", geometry version: " + std::to_string(GeometryStorage::GetVersion()) + + ", geometry pending: " + std::to_string(GeometryStorage::HasPendingUploads()) + ", texture pending: " + std::to_string(TextureStorage::HasPendingUploads()) + - ", mesh dirty/active/completed/handles: " + std::to_string(geometry_snapshot.mesh_dirty) + "/" + - std::to_string(geometry_snapshot.mesh_upload_active) + "/" + - std::to_string(geometry_snapshot.mesh_upload_completed) + "/" + - std::to_string(geometry_snapshot.mesh_upload_handles) + - ", skinned dirty/active/completed/handles: " + - std::to_string(geometry_snapshot.skinned_mesh_dirty) + "/" + - std::to_string(geometry_snapshot.skinned_mesh_upload_active) + "/" + - std::to_string(geometry_snapshot.skinned_mesh_upload_completed) + "/" + - std::to_string(geometry_snapshot.skinned_mesh_upload_handles) + - ", strand dirty/active/completed/handles: " + std::to_string(geometry_snapshot.strand_dirty) + - "/" + std::to_string(geometry_snapshot.strand_upload_active) + "/" + - std::to_string(geometry_snapshot.strand_upload_completed) + "/" + - std::to_string(geometry_snapshot.strand_upload_handles) + ", asset queued: " + std::to_string(snapshot.queued) + ", asset loading CPU: " + std::to_string(snapshot.loading_cpu) + ", asset waiting finalize: " + std::to_string(snapshot.waiting_for_finalize) + diff --git a/README.md b/README.md index 2f65c379..5cbcea47 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Services can extend rendering through `RenderLayer` callbacks for shadow maps, d The SDK job system supports scheduled and immediate parallel work. ECS iteration helpers use jobs to process chunks in parallel. The runtime owns a general worker pool plus named service executors for main-thread callbacks, asset IO, GPU resource work, future rendering work, and background tasks. `JobSystem` and `Jobs` remain compatibility facades over this engine-owned `TaskRuntime` boundary so backend experiments can happen behind the same API. -GPU upload/finalization work that is not full frame rendering is routed through the platform-owned `GpuService` instance on a dedicated GPU executor. `Platform::ImmediateSubmit` forwards into that service, and buffers expose async upload/readback entry points for callers that can keep CPU work moving while GPU transfer work completes. Geometry storage schedules whole-buffer mesh, skinned mesh, and strand uploads on this path and publishes draw ranges only after those uploads complete; partial range updates are still future work. The application frame loop, `RenderLayer::RenderAll`, window rendering, and presentation still run on the main thread; the render executor is reserved for future render-thread work. +GPU upload/finalization work that is not full frame rendering is routed through the platform-owned `GpuService` instance on a dedicated GPU executor. `Platform::ImmediateSubmit` forwards into that service, and buffers expose async upload/readback entry points for callers that can keep CPU work moving while GPU transfer work completes. Geometry storage schedules mesh, skinned mesh, and strand uploads on this path, uses dirty subrange uploads for compacted/appended geometry data, and publishes draw ranges only after those uploads complete. The application frame loop, `RenderLayer::RenderAll`, window rendering, and presentation still run on the main thread; the render executor is reserved for future render-thread work. Interactive project asset loading now dispatches scanned assets as an `AssetManager` batch instead of resolving one pending asset per frame. The asset-service path owns per-handle in-flight state, progress snapshots, and blocking sync access, so concurrent requests wait for the first loader instead of creating duplicate asset instances. Assets can opt into staged async loading by producing a CPU-only payload on the asset-IO executor and applying it later on the bounded main-thread finalization lane. GPU-backed staged assets can also register GPU readiness work; those loads remain `GpuPending` until the tracked GPU jobs complete, and sync access blocks until the asset is fully ready. `Texture2D` uses this path so staged pixel decoding is separated from asynchronous GPU upload, and runtime texture data updates use the same GPU-service-backed upload path while readback/export waits for pending texture GPU work. SDK staged assets also include shader/JSON source assets, scene and native prefab YAML, strands, point clouds, and the SDK YAML-backed render assets that deserialize through the default `IAsset` path. Legacy non-staged paths, especially imported model prefab formats that still run through Assimp and create engine objects during import, continue to finalize on the main thread. `ApplicationInitializationSettings::load_project_assets` can disable headless project asset preloading for tests and tools that want to request assets explicitly. Input is routed from GLFW callbacks through the engine input system and layer event hooks. Timing utilities track frame delta time, fixed timestep state, and update counters. From 28053abe0e123ab51545b2aa09f1270b8a9cb015 Mon Sep 17 00:00:00 2001 From: Bosheng Li Date: Thu, 4 Jun 2026 18:36:38 -0700 Subject: [PATCH 3/3] Fix Python binding format coverage --- PythonBinding/src/PyEvoEngine.cpp | 5 ++--- Scripts/format_cpp.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PythonBinding/src/PyEvoEngine.cpp b/PythonBinding/src/PyEvoEngine.cpp index babab797..5b36d1c4 100644 --- a/PythonBinding/src/PyEvoEngine.cpp +++ b/PythonBinding/src/PyEvoEngine.cpp @@ -67,9 +67,8 @@ bool PyEvoEngine::CaptureCurrentScene(const int resolution_x, const int resoluti ", project idle: " + std::to_string(ProjectManager::IsProjectIdle()) + ", geometry version: " + std::to_string(GeometryStorage::GetVersion()) + ", geometry pending: " + std::to_string(GeometryStorage::HasPendingUploads()) + - ", texture pending: " + std::to_string(TextureStorage::HasPendingUploads()) + - ", asset queued: " + std::to_string(snapshot.queued) + - ", asset loading CPU: " + std::to_string(snapshot.loading_cpu) + + ", texture pending: " + std::to_string(TextureStorage::HasPendingUploads()) + ", asset queued: " + + std::to_string(snapshot.queued) + ", asset loading CPU: " + std::to_string(snapshot.loading_cpu) + ", asset waiting finalize: " + std::to_string(snapshot.waiting_for_finalize) + ", asset GPU pending: " + std::to_string(snapshot.gpu_pending)) return false; diff --git a/Scripts/format_cpp.py b/Scripts/format_cpp.py index 4f453442..90e6bf7d 100644 --- a/Scripts/format_cpp.py +++ b/Scripts/format_cpp.py @@ -23,6 +23,7 @@ "EvoEngine_Packages", "EvoEngine_App", "EvoEngine_Tests", + "PythonBinding", ) DEFAULT_EXTENSIONS = (".cpp", ".hpp")