diff --git a/include/hvt/pageableBuffer/pageFileManager.h b/include/hvt/pageableBuffer/pageFileManager.h new file mode 100644 index 00000000..9bb6d3b9 --- /dev/null +++ b/include/hvt/pageableBuffer/pageFileManager.h @@ -0,0 +1,105 @@ +// Copyright 2025 Autodesk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once + +#include +#include // Constants + +#include +#include +#include +#include +#include +#include + +namespace HVT_NS +{ + +class HdBufferPageHandle; + +struct HVT_API HdFreeListEntry +{ + std::ptrdiff_t offset = 0; + size_t size = 0; + + HdFreeListEntry() = default; + HdFreeListEntry(std::ptrdiff_t offset, size_t size) : offset(offset), size(size) {} +}; + +class HVT_API HdPageFileEntry +{ +public: + HdPageFileEntry(const std::string& filename, size_t pageId); + ~HdPageFileEntry(); + + std::ptrdiff_t FindPageFileGap(size_t size); + size_t NextOffset() const { return mNextOffset; } + bool SetNextOffset(std::ptrdiff_t offset); + void AddFreeListEntry(std::ptrdiff_t offset, size_t size); + void ConsolidateFreeList(); + + size_t PageFileId() const { return mPageId; } + size_t SizeLimit() const { return mSizeLimit; } + const std::string& FileName() const { return mFileName; } + + bool WriteData(std::ptrdiff_t offset, const void* data, size_t size); + bool ReadData(std::ptrdiff_t offset, void* data, size_t size); + +private: + const std::string mFileName; + const size_t mPageId; + const size_t mSizeLimit; + std::ptrdiff_t mNextOffset = 0; + std::vector mFreeList; + bool mFreeListConsolidated = true; + mutable std::mutex mFileMutex; +}; + +class HVT_API HdPageFileManager +{ +public: + ~HdPageFileManager(); + + std::unique_ptr CreatePageHandle(const void* data, size_t size); + bool LoadPage(const HdBufferPageHandle& handle, void* data); + bool UpdatePage(const HdBufferPageHandle& handle, const void* data); + void DeletePage(const HdBufferPageHandle& handle); + + size_t GetTotalDiskUsage() const; + void PrintPagerStats() const; + + static constexpr size_t MAX_PAGE_FILE_SIZE = static_cast(1.8) * ONE_GiB; + +private: + // By design, only HdPageableBufferManager can create and hold it. + HdPageFileManager(std::filesystem::path pageFileDirectory); + + // Disable copy and move + HdPageFileManager(const HdPageFileManager&) = delete; + HdPageFileManager(HdPageFileManager&&) = delete; + + HdPageFileEntry* GetCurrentPageFileEntry() const; + bool CreatePageFile(); + + std::vector> mPageFileEntries; + mutable std::mutex mSyncMutex; + + std::filesystem::path mPageFileDirectory = + std::filesystem::temp_directory_path() / "hvt_temp_pages"; + + template + friend class HdPageableBufferManager; +}; + +} // namespace HVT_NS \ No newline at end of file diff --git a/include/hvt/pageableBuffer/pageableBuffer.h b/include/hvt/pageableBuffer/pageableBuffer.h new file mode 100644 index 00000000..784b3ae3 --- /dev/null +++ b/include/hvt/pageableBuffer/pageableBuffer.h @@ -0,0 +1,221 @@ +// Copyright 2025 Autodesk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace HVT_NS +{ + +// Forward declarations +class HdPageFileManager; +class HdMemoryMonitor; + +template < +#if defined(__cpp_concepts) + HdPagingConcepts::PagingStrategyLike PagingStrategyType, + HdPagingConcepts::BufferSelectionStrategyLike BufferSelectionStrategyType +#else + typename PagingStrategyType, typename BufferSelectionStrategyType +#endif + > +class HdPageableBufferManager; + +enum class HVT_API HdBufferState +{ + Unknown = 0, ///< Initial state + SceneBuffer = 1 << 0, ///< Data in the scene + RendererBuffer = 1 << 1, ///< Data in renderer + DiskBuffer = 1 << 2, ///< Data in disk +}; + +enum class HVT_API HdBufferUsage +{ + Static, ///< Immutable data, will be paged if possible + Dynamic ///< Mutable data, will be paged if necessary +}; + +class HVT_API HdBufferPageHandle +{ +public: + constexpr HdBufferPageHandle(size_t pageId, size_t size, std::ptrdiff_t offset) noexcept : + mPageId(pageId), mSize(size), mOffset(offset) + { + } + + constexpr size_t PageId() const noexcept { return mPageId; } + constexpr size_t Size() const noexcept { return mSize; } + constexpr std::ptrdiff_t Offset() const noexcept { return mOffset; } + constexpr bool IsValid() const noexcept { return mOffset != static_cast(-1); } + + // Comparison operators + bool operator!=(const HdBufferPageHandle& other) const noexcept + { + return mPageId != other.mPageId || mOffset != other.mOffset || mSize != other.mSize; + } + + bool operator==(const HdBufferPageHandle& other) const noexcept { return !(*this != other); } + + bool operator<(const HdBufferPageHandle& other) const noexcept + { + if (mPageId != other.mPageId) + return mPageId < other.mPageId; + if (mOffset != other.mOffset) + return mOffset < other.mOffset; + return mSize < other.mSize; + } + + bool operator>(const HdBufferPageHandle& other) const noexcept { return other < *this; } + + bool operator<=(const HdBufferPageHandle& other) const noexcept { return !(other < *this); } + + bool operator>=(const HdBufferPageHandle& other) const noexcept { return !(*this < other); } + +private: + const size_t mPageId; + const size_t mSize; + const std::ptrdiff_t mOffset; +}; + +// NOTE: Implementation should maintain data consistency. +// For example, once data is swapped out to disk, it's immutable. And if user want to READ/WRITE the +// data in any case, the buffer should be paged back. +class HVT_API HdPageableBufferBase +{ +public: + using DestructionCallback = std::function; + virtual ~HdPageableBufferBase(); + + // Resource management between Scene, Renderer and disk. ////////////////// + // Page: Create new buffer and fill data. Keep the source buffer. + [[nodiscard]] virtual bool PageToSceneMemory(bool force = false); + [[nodiscard]] virtual bool PageToRendererMemory(bool force = false); + [[nodiscard]] virtual bool PageToDisk(bool force = false); + + // Swap: Create new buffer and fill data. Release the source buffer. + [[nodiscard]] virtual bool SwapSceneToDisk(bool force = false, + HdBufferState releaseBuffer = static_cast( + static_cast(HdBufferState::SceneBuffer) | + static_cast(HdBufferState::RendererBuffer))); + [[nodiscard]] virtual bool SwapRendererToDisk(bool force = false, + HdBufferState releaseBuffer = static_cast( + static_cast(HdBufferState::SceneBuffer) | + static_cast(HdBufferState::RendererBuffer))); + // clang-format off + [[nodiscard]] virtual bool SwapToSceneMemory(bool force = false, + HdBufferState releaseBuffer = static_cast( + static_cast(HdBufferState::RendererBuffer) | + static_cast(HdBufferState::DiskBuffer))); + [[nodiscard]] virtual bool SwapToRendererMemory(bool force = false, + HdBufferState releaseBuffer = static_cast( + static_cast(HdBufferState::SceneBuffer) | + static_cast(HdBufferState::DiskBuffer))); + // clang-format on + + // Core operation sets: Release. ////////////////////////////////////////// + // Release: Release the source buffer and update the state. + virtual void ReleaseSceneBuffer() noexcept; + virtual void ReleaseRendererBuffer() noexcept; + virtual void ReleaseDiskPage() noexcept; + + // Get memory as spans for safe access + [[nodiscard]] virtual PXR_NS::TfSpan GetSceneMemorySpan() const noexcept; + [[nodiscard]] virtual PXR_NS::TfSpan GetSceneMemorySpan() noexcept; + [[nodiscard]] virtual PXR_NS::TfSpan GetRendererMemorySpan() const noexcept; + [[nodiscard]] virtual PXR_NS::TfSpan GetRendererMemorySpan() noexcept; + + // Properties + [[nodiscard]] constexpr const PXR_NS::SdfPath& Path() const noexcept { return mPath; } + [[nodiscard]] constexpr size_t Size() const noexcept { return mSize; } + void SetSize(size_t size) noexcept { mSize = size; } + [[nodiscard]] constexpr HdBufferUsage Usage() const noexcept { return mUsage; } + [[nodiscard]] constexpr HdBufferState GetBufferState() const noexcept { return mBufferState; } + + [[nodiscard]] constexpr int FrameStamp() const noexcept { return mFrameStamp; } + void UpdateFrameStamp(int frame) noexcept { mFrameStamp = frame; } + + // Status + [[nodiscard]] constexpr bool IsOverAge(int currentFrame, int ageLimit) const noexcept + { + return (currentFrame - mFrameStamp) > ageLimit; + } + [[nodiscard]] bool HasValidDiskBuffer() const noexcept + { + return mPageHandle && mPageHandle->IsValid(); + } + [[nodiscard]] constexpr bool HasSceneBuffer() const noexcept + { + return (static_cast(mBufferState) & static_cast(HdBufferState::SceneBuffer)); + } + [[nodiscard]] constexpr bool HasRendererBuffer() const noexcept + { + return (static_cast(mBufferState) & static_cast(HdBufferState::RendererBuffer)); + } + [[nodiscard]] constexpr bool HasDiskBuffer() const noexcept + { + return (static_cast(mBufferState) & static_cast(HdBufferState::DiskBuffer)); + } + +protected: + // By design, only HdPageableBufferManager can create buffers. + HdPageableBufferBase(const PXR_NS::SdfPath& path, size_t size, HdBufferUsage usage, + const std::unique_ptr& pageFileManager, + const std::unique_ptr& memoryMonitor, + DestructionCallback destructionCallback); + + // Core operation sets: Creation. ///////////////////////////////////////// + // Create: Create a new buffer and update the state. No data is copied. + virtual void CreateSceneBuffer(); + virtual void CreateRendererBuffer(); + + // Helper to create aligned memory span + template + [[nodiscard]] constexpr PXR_NS::TfSpan MakeSpan( + std::unique_ptr& ptr, size_t size) const noexcept + { + return ptr ? PXR_NS::TfSpan(ptr.get(), size) : PXR_NS::TfSpan {}; + } + + template + friend class HdPageableBufferManager; + + const PXR_NS::SdfPath mPath; // TODO: really need to hold??? + const HdBufferUsage mUsage; + + size_t mSize = 0; + HdBufferState mBufferState = HdBufferState::Unknown; + int mFrameStamp = 0; // Frame stamp for age tracking + + // Page handle for disk storage + std::unique_ptr mPageHandle; + + // Destruction callback to notify BufferManager (and avoid cycle ref) + DestructionCallback mDestructionCallback; + + // Accessor to PageFileManager & MemoryMonitor + std::unique_ptr& mPageFileManager; + std::unique_ptr& mMemoryMonitor; +}; + +} // namespace HVT_NS diff --git a/include/hvt/pageableBuffer/pageableBufferManager.h b/include/hvt/pageableBuffer/pageableBufferManager.h new file mode 100644 index 00000000..df44c872 --- /dev/null +++ b/include/hvt/pageableBuffer/pageableBufferManager.h @@ -0,0 +1,747 @@ +// Copyright 2025 Autodesk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace HVT_NS +{ + +class HdPageableBufferBase; + +template < +#if defined(__cpp_concepts) + HdPagingConcepts::PagingStrategyLike PagingStrategyType, + HdPagingConcepts::BufferSelectionStrategyLike BufferSelectionStrategyType +#else + typename PagingStrategyType, + typename BufferSelectionStrategyType +#endif + > +class HVT_API HdPageableBufferManager +{ +public: +#if !defined(__cpp_concepts) + using BufferSelectionStrategyIterator = tbb::concurrent_unordered_map, PXR_NS::SdfPath::Hash>::iterator; + static_assert(HdPagingConcepts::PagingStrategyLikeValue, + "PagingStrategyType does not meet the requirements of a paging strategy"); + static_assert(HdPagingConcepts::BufferSelectionStrategyLikeValue, + "BufferSelectionStrategyType does not meet the requirements of a buffer selection " + "strategy"); +#endif + + struct InitializeDesc + { + std::filesystem::path pageFileDirectory = + std::filesystem::temp_directory_path() / "hvt_temp_pages"; + int ageLimit = 20; ///< frame count + size_t sceneMemoryLimit = static_cast(2) * ONE_GiB; + size_t rendererMemoryLimit = static_cast(1) * ONE_GiB; + size_t numThreads = 0; ///< 0 means disable async operations + }; + // Constructor and destructor are now public for direct instantiation + HdPageableBufferManager(InitializeDesc desc) : + mAgeLimit(desc.ageLimit), + mPageFileManager( + std::unique_ptr(new HdPageFileManager(desc.pageFileDirectory))), + mMemoryMonitor(std::unique_ptr( + new HdMemoryMonitor(desc.sceneMemoryLimit, desc.rendererMemoryLimit))) + { + if (desc.numThreads > 0) + { + mTaskArena = std::make_unique(desc.numThreads); + mTaskArena->execute([this]() { mTaskGroup = std::make_unique(); }); + } + } + + ~HdPageableBufferManager() + { + // Wait for all pending tasks to complete before destruction + WaitForAllOperations(); + + // Ensure all resources are released before destructing the manager. + if (mTaskGroup && mTaskArena) + { + mTaskArena->execute([this]() + { + mTaskGroup.reset(); + }); + } + mTaskArena.reset(); + mBuffers.clear(); + } + + // Frame stamp management + void AdvanceFrame(uint advanceCount = 1) noexcept { mCurrentFrame += advanceCount; } + [[nodiscard]] constexpr uint GetCurrentFrame() const noexcept { return mCurrentFrame; } + + // Strategy access (no runtime changing allowed) + [[nodiscard]] constexpr PagingStrategyType GetPagingStrategy() const noexcept + { + return mPagingStrategy; + } + [[nodiscard]] constexpr BufferSelectionStrategyType GetBufferSelectionStrategy() const noexcept + { + return mBufferSelectionStrategy; + } + constexpr int GetAgeLimit() const noexcept { return mAgeLimit; } + + // Accessors to internal managers + [[nodiscard]] std::unique_ptr& GetPageFileManager() + { + return mPageFileManager; + } + [[nodiscard]] std::unique_ptr& GetMemoryMonitor() { return mMemoryMonitor; } + + // Buffer operations ////////////////////////////////////////////////////// + + // Buffer lifecycle management + [[nodiscard]] std::shared_ptr CreateBuffer( + const PXR_NS::SdfPath& path, size_t size = 0, HdBufferUsage usage = HdBufferUsage::Static); + bool AddBuffer(const PXR_NS::SdfPath& path, std::shared_ptr buffer); + void RemoveBuffer(const PXR_NS::SdfPath& path); + [[nodiscard]] std::shared_ptr FindBuffer(const PXR_NS::SdfPath& path); + + // Paging trigger + static constexpr size_t kMinimalCheckCount = 10; + void FreeCrawl(float percentage = 10.0f); + + // Async buffer operations + [[nodiscard]] std::future PageToSceneMemoryAsync( + std::shared_ptr buffer, bool force = false); + [[nodiscard]] std::future PageToRendererMemoryAsync( + std::shared_ptr buffer, bool force = false); + [[nodiscard]] std::future PageToDiskAsync( + std::shared_ptr buffer, bool force = false); + + [[nodiscard]] std::future SwapSceneToDiskAsync( + std::shared_ptr buffer, bool force = false); + [[nodiscard]] std::future SwapRendererToDiskAsync( + std::shared_ptr buffer, bool force = false); + [[nodiscard]] std::future SwapToSceneMemoryAsync( + std::shared_ptr buffer, bool force = false, + HdBufferState releaseBuffer = static_cast( + static_cast(HdBufferState::RendererBuffer) | + static_cast(HdBufferState::DiskBuffer))); + [[nodiscard]] std::future SwapToRendererMemoryAsync( + std::shared_ptr buffer, bool force = false, + HdBufferState releaseBuffer = static_cast( + static_cast(HdBufferState::SceneBuffer) | + static_cast(HdBufferState::DiskBuffer))); + + [[nodiscard]] std::future ReleaseSceneBufferAsync( + std::shared_ptr buffer) noexcept; + [[nodiscard]] std::future ReleaseRendererBufferAsync( + std::shared_ptr buffer) noexcept; + [[nodiscard]] std::future ReleaseDiskPageAsync( + std::shared_ptr buffer) noexcept; + + // Async paging trigger. + std::vector> FreeCrawlAsync(float percentage = 10.0f); + + // Async operation status + size_t GetPendingOperations() const; + void WaitForAllOperations(); + + // Statistics + // NOTE: These APIs may severely slow down the system and should be used for development only. + [[nodiscard]] size_t GetBufferCount() const; + void PrintCacheStats() const; + +private: + // Disable copy and move + HdPageableBufferManager(const HdPageableBufferManager&) = delete; + HdPageableBufferManager(HdPageableBufferManager&&) = delete; + + // HdPageableBufferBase destruction callback + void OnBufferDestroyed(const PXR_NS::SdfPath& path); + + // Helper method: dispose old buffer using configurable strategy + bool DisposeOldBuffer(HdPageableBufferBase& buffer, int currentFrame, int ageLimit, + float scenePressure, float rendererPressure); + + // Execute paging decision on buffer (synchronous) + bool ExecutePagingDecision(HdPageableBufferBase& buffer, const HdPagingDecision& decision); + + // Execute paging decision on buffer (asynchronous) + std::future ExecutePagingDecisionAsync( + std::shared_ptr buffer, const HdPagingDecision& decision); + + // Helper method for creating tasks with trackable future + template + std::future> SubmitTask(Callable&& task); + + tbb::concurrent_unordered_map, + PXR_NS::SdfPath::Hash> + mBuffers; + + std::atomic mCurrentFrame { 0 }; + const int mAgeLimit { 20 }; // TODO: move to strategies??? + + // Compile-time strategy instances (no runtime changing) + PagingStrategyType mPagingStrategy {}; + BufferSelectionStrategyType mBufferSelectionStrategy {}; + + std::unique_ptr mPageFileManager; + std::unique_ptr mMemoryMonitor; + + // Members for async buffer operations + std::unique_ptr mTaskArena; + std::unique_ptr mTaskGroup; + std::atomic mPendingTaskCount { 0 }; +}; + +// Template Methods Implementations /////////////////////////////////////////// + +template +std::shared_ptr HdPageableBufferManager::CreateBuffer(const PXR_NS::SdfPath& path, size_t size, + HdBufferUsage usage) +{ + // Check if buffer with this path already exists + if (const auto it = mBuffers.find(path); it != mBuffers.end()) + { + using namespace PXR_NS; + TF_WARN("HdPageableBufferBase '%s' already exists, returning existing buffer\n", + path.GetText()); + return it->second; + } + + // Create destruction callback that will remove buffer from the list. + auto destructionCallback = [this](const PXR_NS::SdfPath& path) + { this->OnBufferDestroyed(path); }; + + // Create new buffer and insert into the managed list. + auto buffer = std::shared_ptr(new HdPageableBufferBase( + path, size, usage, this->mPageFileManager, this->mMemoryMonitor, destructionCallback)); + { + mBuffers.emplace(path, buffer); + } + return buffer; +} + +template +bool HdPageableBufferManager::AddBuffer( + const PXR_NS::SdfPath& path, std::shared_ptr buffer) +{ + return mBuffers.emplace(path, buffer).second; +} + +template +void HdPageableBufferManager::OnBufferDestroyed( + const PXR_NS::SdfPath& path) +{ + this->RemoveBuffer(path); +} + +template +void HdPageableBufferManager::RemoveBuffer( + const PXR_NS::SdfPath& path) +{ + if (auto it = mBuffers.find(path); it != mBuffers.end()) + { + mBuffers.unsafe_erase(it); + } +} + +template +std::shared_ptr HdPageableBufferManager::FindBuffer(const PXR_NS::SdfPath& path) +{ + if (const auto it = mBuffers.find(path); it != mBuffers.end()) + { + return it->second; + } + return nullptr; +} + +template +bool HdPageableBufferManager::DisposeOldBuffer( + HdPageableBufferBase& buffer, int currentFrame, int ageLimit, float scenePressure, + float rendererPressure) +{ + // Create paging context + HdPagingContext context; + context.bufferAge = currentFrame - buffer.FrameStamp(); + context.ageLimit = ageLimit; + context.scenePressure = scenePressure; + context.rendererPressure = rendererPressure; + context.isOverAge = buffer.IsOverAge(currentFrame, ageLimit); + context.bufferUsage = buffer.Usage(); + context.bufferState = buffer.GetBufferState(); + + // Use configured strategy + HdPagingDecision decision = mPagingStrategy(buffer, context); + return ExecutePagingDecision(buffer, decision); +} + +template +bool HdPageableBufferManager::ExecutePagingDecision(HdPageableBufferBase& buffer, + const HdPagingDecision& decision) +{ + if (!decision.shouldPage) + { + return false; + } + + bool disposed = false; + + switch (decision.action) + { + case HdPagingDecision::Action::SwapSceneToDisk: + disposed = buffer.SwapSceneToDisk(decision.forceOperation); + break; + + case HdPagingDecision::Action::SwapRendererToDisk: + disposed = buffer.SwapRendererToDisk(decision.forceOperation); + break; + + case HdPagingDecision::Action::SwapToSceneMemory: + disposed = buffer.SwapToSceneMemory(decision.forceOperation); + break; + + case HdPagingDecision::Action::ReleaseRendererBuffer: + buffer.ReleaseRendererBuffer(); + disposed = true; + break; + + case HdPagingDecision::Action::None: + default: + break; + } + + return disposed; +} + +template +std::future HdPageableBufferManager::ExecutePagingDecisionAsync(std::shared_ptr + buffer, + const HdPagingDecision& decision) +{ + if (!mTaskArena || !mTaskGroup) + { + // Return a invalid future + return {}; + } + if (!decision.shouldPage) + { + // Return a future that immediately resolves to false + std::promise promise; + promise.set_value(false); + return promise.get_future(); + } + + switch (decision.action) + { + case HdPagingDecision::Action::SwapSceneToDisk: + return SwapSceneToDiskAsync(buffer, decision.forceOperation); + + case HdPagingDecision::Action::SwapRendererToDisk: + return SwapRendererToDiskAsync(buffer, decision.forceOperation); + + case HdPagingDecision::Action::SwapToSceneMemory: + return SwapToSceneMemoryAsync(buffer, decision.forceOperation); + + case HdPagingDecision::Action::ReleaseRendererBuffer: + return SubmitTask( + [buffer]() -> bool + { + buffer->ReleaseRendererBuffer(); + return true; + }); + + case HdPagingDecision::Action::None: + default: + // Return a future that immediately resolves to false + std::promise promise; + promise.set_value(false); + return promise.get_future(); + } +} + +template +void HdPageableBufferManager::FreeCrawl( + float percentage) +{ + float scenePressure = mMemoryMonitor->GetSceneMemoryPressure(); + float rendererPressure = mMemoryMonitor->GetRendererMemoryPressure(); + + // Only crawl if we're under memory pressure + if (scenePressure < HdMemoryMonitor::LOW_MEMORY_THRESHOLD && + rendererPressure < HdMemoryMonitor::LOW_MEMORY_THRESHOLD) + { + return; + } + + // Calculate number of non-null buffers to check + auto numToCheck = static_cast(mBuffers.size() * (percentage / 100.0f)); + numToCheck = std::max(numToCheck, kMinimalCheckCount); + numToCheck = std::min(numToCheck, mBuffers.size()); + + for (auto it = mBuffers.begin(); it != mBuffers.end();) + { + if (!it->second) + { + it = mBuffers.unsafe_erase(it); + } + else + { + ++it; + } + } + + // Create selection context + HdSelectionContext selectionContext; + selectionContext.currentFrame = mCurrentFrame; + selectionContext.requestedCount = numToCheck; + + // Use configurable buffer selection strategy + std::vector> selectedBuffers = + mBufferSelectionStrategy(mBuffers.begin(), mBuffers.end(), selectionContext); + + for (auto& buffer : selectedBuffers) + { + if (buffer) + { + buffer->UpdateFrameStamp(mCurrentFrame); + DisposeOldBuffer(*buffer, mCurrentFrame, mAgeLimit, scenePressure, rendererPressure); + } + } +} + +template +std::vector> HdPageableBufferManager::FreeCrawlAsync(float percentage) +{ + if (!mTaskArena || !mTaskGroup) + { + return {}; + } + + std::vector> futures; + + float scenePressure = mMemoryMonitor->GetSceneMemoryPressure(); + float rendererPressure = mMemoryMonitor->GetRendererMemoryPressure(); + + // Only crawl if we're under memory pressure + if (scenePressure < HdMemoryMonitor::LOW_MEMORY_THRESHOLD && + rendererPressure < HdMemoryMonitor::LOW_MEMORY_THRESHOLD) + { + return futures; + } + // Calculate number of buffers to check + auto numToCheck = static_cast(mBuffers.size() * (percentage / 100.0f)); + numToCheck = std::max(numToCheck, kMinimalCheckCount); + numToCheck = std::min(numToCheck, mBuffers.size()); + + // Remove null buffers first + for (auto it = mBuffers.begin(); it != mBuffers.end();) + { + if (!it->second) + { + it = mBuffers.unsafe_erase(it); + } + else + { + ++it; + } + } + + // Create selection context + HdSelectionContext selectionContext; + selectionContext.currentFrame = mCurrentFrame; + selectionContext.requestedCount = numToCheck; + + // Use configurable buffer selection strategy + std::vector> selectedBuffers = + mBufferSelectionStrategy(mBuffers.begin(), mBuffers.end(), selectionContext); + + // Start async operations for each selected buffer + for (auto& buffer : selectedBuffers) + { + if (!buffer) + { + continue; + } + + buffer->UpdateFrameStamp(mCurrentFrame); + + // Check if buffer should be disposed based on age and pressure + int age = mCurrentFrame - buffer->FrameStamp(); + if (age >= mAgeLimit) + { + // Create paging context and get decision + HdPagingContext context; + context.ageLimit = mAgeLimit; + context.scenePressure = scenePressure; + context.rendererPressure = rendererPressure; + + HdPagingDecision decision = mPagingStrategy(*buffer, context); + + // Start async operation + if (decision.shouldPage) + { + futures.push_back(ExecutePagingDecisionAsync(buffer, decision)); + } + } + } + return futures; +} + +template +size_t HdPageableBufferManager::GetBufferCount() + const +{ +#ifdef _DEBUG + const auto nonEmptyBuffer = std::count_if( + mBuffers.begin(), mBuffers.end(), [](const auto& buffer) { return buffer != nullptr; }); + if (nonEmptyBuffer != mBuffers.size()) + { + using namespace PXR_NS; + TF_STATUS("HdPageableBufferManager::GetBufferCount find %zu empty buffers.\n", + mBuffers.size() - nonEmptyBuffer); + } +#endif + return mBuffers.size(); +} + +template +void HdPageableBufferManager::PrintCacheStats() + const +{ + using namespace PXR_NS; + size_t sceneBuffers = 0; + size_t rendererBuffers = 0; + size_t diskBuffers = 0; + for (auto const& it : mBuffers) + { + if (!it.second) + continue; + + if (it.second->HasSceneBuffer()) + ++sceneBuffers; + if (it.second->HasRendererBuffer()) + ++rendererBuffers; + if (it.second->HasDiskBuffer()) + ++diskBuffers; + } + + TF_STATUS( + "\n=== Cache Statistics ===\n" + "Total Buffers: %zu\n" + "Scene Buffers: %zu\n" + "Renderer Buffers: %zu\n" + "Disk Buffers: %zu\n" + "Current Frame: %u\n" + "Age Limit: %d frames\n" + "========================\n", + mBuffers.size(), sceneBuffers, rendererBuffers, diskBuffers, mCurrentFrame.load(), + mAgeLimit); +} + +// Async Buffer Operations //////////////////////////////////////////////////// + +template +size_t HdPageableBufferManager::GetPendingOperations() const +{ + return (!mTaskArena || !mTaskGroup) ? 0 : mPendingTaskCount.load(); +} + +template +void HdPageableBufferManager::WaitForAllOperations() +{ + if (!mTaskArena || !mTaskGroup) + { + return; + } + + mTaskArena->execute([this]() + { + mTaskGroup->wait(); + }); + + // Reset pending count after all tasks complete + mPendingTaskCount.store(0); +} + +// Helper method for submitting tasks with TBB task_group and future support /// +template +template +std::future> HdPageableBufferManager::SubmitTask(Callable&& task) +{ + using ResultType = std::invoke_result_t; + + if (!mTaskArena || !mTaskGroup) + { + // Return an invalid future if async operations are not initialized + return std::future(); + } + + // Create a packaged_task to get a future + auto packagedTask = + std::make_shared>(std::forward(task)); + auto future = packagedTask->get_future(); + + // Submit task + mPendingTaskCount.fetch_add(1); + mTaskArena->execute([this, packagedTask]() + { + mTaskGroup->run( + [this, packagedTask]() + { + (*packagedTask)(); + mPendingTaskCount.fetch_sub(1); + }); + }); + + return future; +} + +template +std::future HdPageableBufferManager:: + PageToSceneMemoryAsync(std::shared_ptr buffer, bool force) +{ + return SubmitTask([buffer, force]() -> bool { return buffer->PageToSceneMemory(force); }); +} + +template +std::future HdPageableBufferManager:: + PageToRendererMemoryAsync(std::shared_ptr buffer, bool force) +{ + return SubmitTask([buffer, force]() -> bool { return buffer->PageToRendererMemory(force); }); +} + +template +std::future HdPageableBufferManager::PageToDiskAsync(std::shared_ptr buffer, + bool force) +{ + return SubmitTask([buffer, force]() -> bool { return buffer->PageToDisk(force); }); +} + +template +std::future HdPageableBufferManager::SwapSceneToDiskAsync(std::shared_ptr buffer, + bool force) +{ + return SubmitTask([buffer, force]() -> bool { return buffer->SwapSceneToDisk(force); }); +} + +template +std::future HdPageableBufferManager:: + SwapRendererToDiskAsync(std::shared_ptr buffer, bool force) +{ + return SubmitTask([buffer, force]() -> bool { return buffer->SwapRendererToDisk(force); }); +} + +template +std::future HdPageableBufferManager::SwapToSceneMemoryAsync(std::shared_ptr + buffer, + bool force, HdBufferState releaseBuffer) +{ + return SubmitTask([buffer, force, releaseBuffer]() -> bool + { return buffer->SwapToSceneMemory(force, releaseBuffer); }); +} + +template +std::future HdPageableBufferManager::SwapToRendererMemoryAsync(std::shared_ptr + buffer, + bool force, HdBufferState releaseBuffer) +{ + return SubmitTask([buffer, force, releaseBuffer]() -> bool + { return buffer->SwapToRendererMemory(force, releaseBuffer); }); +} + +template +std::future HdPageableBufferManager:: + ReleaseSceneBufferAsync(std::shared_ptr buffer) noexcept +{ + return SubmitTask([buffer]() -> void { buffer->ReleaseSceneBuffer(); }); +} + +template +std::future HdPageableBufferManager:: + ReleaseRendererBufferAsync(std::shared_ptr buffer) noexcept +{ + return SubmitTask([buffer]() -> void { buffer->ReleaseRendererBuffer(); }); +} + +template +std::future HdPageableBufferManager:: + ReleaseDiskPageAsync(std::shared_ptr buffer) noexcept +{ + return SubmitTask([buffer]() -> void { buffer->ReleaseDiskPage(); }); +} + +// Built-in BufferManager Aliases ///////////////////////////////////////////// + +// Default HdPageableBufferManager (also the one offered in HdMemoryManager) +using DefaultBufferManager = HdPageableBufferManager; + +// Memory-focused combinations +using PressureBasedLargestBufferManager = + HdPageableBufferManager; +using PressureBasedLRUBufferManager = + HdPageableBufferManager; + +// Performance-focused combinations +using ConservativeFIFOBufferManager = + HdPageableBufferManager; +using ConservativeOldestBufferManager = + HdPageableBufferManager; + +// Strategy-specific combinations +using AgeBasedBufferManager = HdPageableBufferManager; +using FIFOBufferManager = HdPageableBufferManager; + +} // namespace HVT_NS diff --git a/include/hvt/pageableBuffer/pageableConcepts.h b/include/hvt/pageableBuffer/pageableConcepts.h new file mode 100644 index 00000000..53162c9e --- /dev/null +++ b/include/hvt/pageableBuffer/pageableConcepts.h @@ -0,0 +1,240 @@ +// Copyright 2025 Autodesk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once + +#include + +#include +#include + +#include +#include +#if defined(__cpp_concepts) +#include +#else +#include +#include +#endif + +namespace HVT_NS +{ + +// Forward declarations +class HdPageableBufferBase; +struct HdPagingContext; +struct HdPagingDecision; +struct HdSelectionContext; + +// clang-format off +namespace HdPagingConcepts +{ + +#if defined(__cpp_concepts) + +// Concept for objects that have an id (path) +template +concept Pathed = requires(T t) +{ + { t.Path() } -> std::convertible_to; +}; + +// Concept for objects that have a size +template +concept Sized = requires(T t) +{ + { t.Size() } -> std::convertible_to; +}; + +// Concept for memory-managed objects +template +concept MemoryManaged = requires(T t) +{ + { t.PageToSceneMemory() } -> std::convertible_to; + { t.PageToRendererMemory() } -> std::convertible_to; + { t.PageToDisk() } -> std::convertible_to; +}; + +// Concept for aged resources +template +concept Aged = requires(T t, int frame) +{ + { t.FrameStamp() } -> std::convertible_to; + { t.UpdateFrameStamp(frame) } -> std::same_as; + { t.IsOverAge(frame, frame) } -> std::convertible_to; +}; + +// Combined concept for complete buffer-like objects +template +concept BufferLike = Pathed && Sized && MemoryManaged && Aged; + +// Concept for pageable buffer managers +template +concept BufferManagerLike = + requires(T t, std::shared_ptr buffer, PXR_NS::SdfPath path, size_t size) +{ + requires BufferLike; + { t.CreateBuffer(path, size) } -> std::same_as>; + { t.RemoveBuffer(path) } -> std::same_as; + { t.FindBuffer(path) } -> std::convertible_to>; + { t.FreeCrawl() } -> std::same_as; +}; + +// Concept for paging strategies +template +concept PagingStrategyLike = + requires(T t, const HdPageableBufferBase& buffer, const HdPagingContext& context) +{ + { t(buffer, context) } -> std::convertible_to; +} +|| requires(T t, const HdPageableBufferBase& buffer, const HdPagingContext& context) +{ + { t.operator()(buffer, context) } -> std::convertible_to; +}; + +// Concept for buffer selection strategies +template +concept BufferSelectionStrategyLike = + requires(T t, InputIterator first, InputIterator last, const HdSelectionContext& context) +{ + { t(first, last, context) } + -> std::convertible_to>>; +} +|| requires(T t, InputIterator first, InputIterator last, const HdSelectionContext& context) +{ + { t.operator()(first, last, context) } + -> std::convertible_to>>; +}; + +#else // !defined(__cpp_concepts) + +// SFINAE-based detection helpers for pre-C++20 compilers + +template +struct Pathed : std::false_type {}; +template +struct Pathed().Path())>> + : std::is_convertible().Path()), PXR_NS::SdfPath> {}; + +template +struct Sized : std::false_type {}; +template +struct Sized().Size())>> + : std::is_convertible().Size()), std::size_t> {}; + +template +struct MemoryManaged : std::false_type {}; +template +struct MemoryManaged().PageToSceneMemory()), + decltype(std::declval().PageToRendererMemory()), + decltype(std::declval().PageToDisk())>> +{ + static constexpr bool value = + std::is_convertible().PageToSceneMemory()), bool>::value && + std::is_convertible().PageToRendererMemory()), bool>::value && + std::is_convertible().PageToDisk()), bool>::value; +}; + +template +struct Aged : std::false_type {}; +template +struct Aged().FrameStamp()), + decltype(std::declval().UpdateFrameStamp(0)), + decltype(std::declval().IsOverAge(0, 0))>> +{ + static constexpr bool value = + std::is_convertible().FrameStamp()), int>::value && + std::is_same().UpdateFrameStamp(0)), void>::value && + std::is_convertible().IsOverAge(0, 0)), bool>::value; +}; + +// Traits that resemble pageable concepts + +template +struct BufferLike + : std::integral_constant::value && Sized::value && MemoryManaged::value && Aged::value> {}; +template +inline constexpr bool BufferLikeValue = BufferLike::value; + +template +struct _BufferManagerLike : std::false_type {}; +template +struct _BufferManagerLike().CreateBuffer( + std::declval(), std::declval())), + decltype(std::declval().RemoveBuffer(std::declval())), + decltype(std::declval().FindBuffer(std::declval())), + decltype(std::declval().FreeCrawl())>> +{ + static constexpr bool value = BufferLikeValue && + std::is_same().CreateBuffer(std::declval(), std::declval())), + std::shared_ptr>::value && + std::is_same().RemoveBuffer(std::declval())), void>::value && + std::is_convertible().FindBuffer(std::declval())), std::shared_ptr>::value && + std::is_same().FreeCrawl()), void>::value; +}; + +template +struct BufferManagerLike : std::integral_constant::value> {}; +template +inline constexpr bool BufferManagerLikeValue = BufferManagerLike::value; + +template +struct _PagingStrategyLike : std::false_type {}; +template +struct _PagingStrategyLike()( + std::declval(), std::declval()))>> +{ + static constexpr bool value = + std::is_convertible()( + std::declval(), std::declval())), + HdPagingDecision>::value; +}; + +template +struct PagingStrategyLike : std::integral_constant::value> {}; +template +inline constexpr bool PagingStrategyLikeValue = PagingStrategyLike::value; + +template +struct _BufferSelectionStrategyLike : std::false_type {}; +template +struct _BufferSelectionStrategyLike()(std::declval(), + std::declval(), std::declval()))>> +{ + static constexpr bool value = + std::is_convertible()(std::declval(), + std::declval(), std::declval())), + std::vector>>::value; +}; + +template +struct BufferSelectionStrategyLike + : std::integral_constant::value> {}; +template +inline constexpr bool BufferSelectionStrategyLikeValue = + BufferSelectionStrategyLike::value; + +#endif // __cpp_concepts + +} // namespace HdPagingConcepts +// clang-format on +} // namespace HVT_NS diff --git a/include/hvt/pageableBuffer/pageableDataSource.h b/include/hvt/pageableBuffer/pageableDataSource.h new file mode 100644 index 00000000..dc247200 --- /dev/null +++ b/include/hvt/pageableBuffer/pageableDataSource.h @@ -0,0 +1,291 @@ +// Copyright 2025 Autodesk, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace HVT_NS +{ + +/// Forward declarations +class HdMemoryManager; + +/// Memory-aware VtValue. +// TODO: We should either forbid users from modifying the source VtArray once it is paged out. +// or, remove the HdPageableValue design but make it more higher level and applicable to data +// source only. +class HVT_API HdPageableValue : public HdPageableBufferBase +{ +public: + HdPageableValue(const PXR_NS::SdfPath& path, size_t estimatedSize, HdBufferUsage usage, + const std::unique_ptr& pageFileManager, + const std::unique_ptr& memoryMonitor, + DestructionCallback destructionCallback, const PXR_NS::VtValue& data, + const PXR_NS::TfToken& dataType); + + /// Get the original VtValue data (triggers load if needed) + PXR_NS::VtValue GetValue(); + + /// Get data type for Hydra consumption + PXR_NS::TfToken GetDataType() const { return mDataType; } + + /// Check if data is immediately available + bool IsDataResident() const { return HdPageableBufferBase::HasSceneBuffer(); } + + /// HdPageableBufferBase methods ////////////////////////////////////////// + + bool SwapSceneToDisk(bool force = false, + HdBufferState releaseBuffer = static_cast( + static_cast(HdBufferState::RendererBuffer) | + static_cast(HdBufferState::DiskBuffer))) override; + bool SwapToSceneMemory( + bool force = false, HdBufferState releaseBuffer = HdBufferState::DiskBuffer) override; + + [[nodiscard]] PXR_NS::TfSpan GetSceneMemorySpan() const noexcept override; + [[nodiscard]] PXR_NS::TfSpan GetSceneMemorySpan() noexcept override; + + /// Utilities + static size_t EstimateMemoryUsage(const PXR_NS::VtValue& value) noexcept; + std::vector SerializeVtValue(const PXR_NS::VtValue& value) const noexcept; + PXR_NS::VtValue DeserializeVtValue(const std::vector& data) noexcept; + +private: + mutable PXR_NS::VtValue mSourceValue; + PXR_NS::TfToken mDataType; +}; + +struct HVT_API HdContainerPageEntry +{ + std::type_index type; + size_t offset; + size_t size; +}; + +/// Memory-managed container data source. +class HVT_API HdPageableContainerDataSource : public PXR_NS::HdContainerDataSource, + public HdPageableBufferBase +{ +public: + HD_DECLARE_DATASOURCE_ABSTRACT(HdPageableContainerDataSource); + + static Handle New(const PXR_NS::SdfPath& primPath, + const std::unique_ptr& pageFileManager, + const std::unique_ptr& memoryMonitor, + DestructionCallback destructionCallback, HdBufferUsage usage = HdBufferUsage::Static); + + virtual std::map GetMemoryBreakdown() const; + +private: + HdPageableContainerDataSource(const PXR_NS::SdfPath& primPath, + const std::unique_ptr& pageFileManager, + const std::unique_ptr& memoryMonitor, + DestructionCallback destructionCallback, HdBufferUsage usage = HdBufferUsage::Static); + + std::map mContainerPageEntries; +}; +HD_DECLARE_DATASOURCE_HANDLES(HdPageableContainerDataSource); + +/// Memory-managed vector data source. +class HVT_API HdPageableVectorDataSource : public PXR_NS::HdVectorDataSource, + public HdPageableBufferBase +{ +public: + HD_DECLARE_DATASOURCE_ABSTRACT(HdPageableVectorDataSource); + + static Handle New(const PXR_NS::SdfPath& primPath, + const std::unique_ptr& pageFileManager, + const std::unique_ptr& memoryMonitor, + DestructionCallback destructionCallback, HdBufferUsage usage = HdBufferUsage::Static); + + virtual std::vector GetMemoryBreakdown() const; + +private: + HdPageableVectorDataSource(const PXR_NS::SdfPath& primPath, + const std::unique_ptr& pageFileManager, + const std::unique_ptr& memoryMonitor, + DestructionCallback destructionCallback, HdBufferUsage usage = HdBufferUsage::Static); + + std::vector mElements; +}; +HD_DECLARE_DATASOURCE_HANDLES(HdPageableVectorDataSource); + +// TODO ???? +/// Memory-managed sampled data source for time-sampled values. +class HVT_API HdPageableSampledDataSource : public PXR_NS::HdSampledDataSource, + public HdPageableBufferBase +{ +public: + HD_DECLARE_DATASOURCE_ABSTRACT(HdPageableSampledDataSource); + + /// Create with memory management + static Handle New(const PXR_NS::VtValue& value, const PXR_NS::SdfPath& primPath, + const PXR_NS::TfToken& attributeName, + const std::unique_ptr& pageFileManager, + const std::unique_ptr& memoryMonitor, + DestructionCallback destructionCallback, HdBufferUsage usage = HdBufferUsage::Static); + + /// Create time-sampled with memory management + static Handle New(const std::map& samples, + const PXR_NS::SdfPath& primPath, const PXR_NS::TfToken& attributeName, + const std::unique_ptr& pageFileManager, + const std::unique_ptr& memoryMonitor, + DestructionCallback destructionCallback, HdBufferUsage usage = HdBufferUsage::Static); + + /// HdSampledDataSource interface + PXR_NS::VtValue GetValue(Time shutterOffset) override; + bool GetContributingSampleTimesForInterval( + Time startTime, Time endTime, std::vector