// // Copyright 2020 The ANGLE Project Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // CommandQueue.h: // A class to process and submit Vulkan command buffers. // #ifndef LIBANGLE_RENDERER_VULKAN_COMMAND_Queue_H_ #define LIBANGLE_RENDERER_VULKAN_COMMAND_Queue_H_ #include #include #include #include #include "common/FixedQueue.h" #include "common/SimpleMutex.h" #include "common/vulkan/vk_headers.h" #include "libANGLE/renderer/vulkan/PersistentCommandPool.h" #include "libANGLE/renderer/vulkan/vk_helpers.h" #include "vulkan/vulkan_core.h" namespace rx { namespace vk { class ExternalFence; using SharedExternalFence = std::shared_ptr; constexpr size_t kInFlightCommandsLimit = 50u; constexpr size_t kMaxFinishedCommandsLimit = 64u; static_assert(kInFlightCommandsLimit <= kMaxFinishedCommandsLimit); struct Error { VkResult errorCode; const char *file; const char *function; uint32_t line; }; class FenceRecycler { public: FenceRecycler() {} ~FenceRecycler() {} void destroy(ErrorContext *context); void fetch(VkDevice device, Fence *fenceOut); void recycle(Fence &&fence); private: angle::SimpleMutex mMutex; Recycler mRecycler; }; class RecyclableFence final : angle::NonCopyable { public: RecyclableFence(); ~RecyclableFence(); VkResult init(VkDevice device, FenceRecycler *recycler); // Returns fence back to the recycler if it is still attached, destroys the fence otherwise. // Do NOT call directly when object is controlled by a shared pointer. void destroy(VkDevice device); void detachRecycler() { mRecycler = nullptr; } bool valid() const { return mFence.valid(); } const Fence &get() const { return mFence; } private: Fence mFence; FenceRecycler *mRecycler; }; using SharedFence = AtomicSharedPtr; class CommandPoolAccess; class CommandBatch final : angle::NonCopyable { public: CommandBatch(); ~CommandBatch(); CommandBatch(CommandBatch &&other); CommandBatch &operator=(CommandBatch &&other); void destroy(VkDevice device); angle::Result release(ErrorContext *context); void setQueueSerial(const QueueSerial &serial); void setProtectionType(ProtectionType protectionType); void setPrimaryCommands(PrimaryCommandBuffer &&primaryCommands, CommandPoolAccess *commandPoolAccess); void setSecondaryCommands(SecondaryCommandBufferCollector &&secondaryCommands); VkResult initFence(VkDevice device, FenceRecycler *recycler); void setExternalFence(SharedExternalFence &&externalFence); const QueueSerial &getQueueSerial() const; const PrimaryCommandBuffer &getPrimaryCommands() const; const SharedExternalFence &getExternalFence(); bool hasFence() const; VkFence getFenceHandle() const; VkResult getFenceStatus(VkDevice device) const; VkResult waitFence(VkDevice device, uint64_t timeout) const; VkResult waitFenceUnlocked(VkDevice device, uint64_t timeout, std::unique_lock *lock) const; private: QueueSerial mQueueSerial; ProtectionType mProtectionType; PrimaryCommandBuffer mPrimaryCommands; CommandPoolAccess *mCommandPoolAccess; // reference to CommandPoolAccess that is responsible // for deleting primaryCommands with a lock SecondaryCommandBufferCollector mSecondaryCommands; SharedFence mFence; SharedExternalFence mExternalFence; }; using CommandBatchQueue = angle::FixedQueue; class DeviceQueueMap; class QueueFamily final : angle::NonCopyable { public: static const uint32_t kInvalidIndex = std::numeric_limits::max(); static uint32_t FindIndex(const std::vector &queueFamilyProperties, VkQueueFlags includeFlags, VkQueueFlags optionalFlags, VkQueueFlags excludeFlags, uint32_t *matchCount); static const uint32_t kQueueCount = static_cast(egl::ContextPriority::EnumCount); static const float kQueuePriorities[static_cast(egl::ContextPriority::EnumCount)]; QueueFamily() : mProperties{}, mQueueFamilyIndex(kInvalidIndex) {} ~QueueFamily() {} void initialize(const VkQueueFamilyProperties &queueFamilyProperties, uint32_t queueFamilyIndex); bool valid() const { return (mQueueFamilyIndex != kInvalidIndex); } uint32_t getQueueFamilyIndex() const { return mQueueFamilyIndex; } const VkQueueFamilyProperties *getProperties() const { return &mProperties; } bool isGraphics() const { return ((mProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT) > 0); } bool isCompute() const { return ((mProperties.queueFlags & VK_QUEUE_COMPUTE_BIT) > 0); } bool supportsProtected() const { return ((mProperties.queueFlags & VK_QUEUE_PROTECTED_BIT) > 0); } uint32_t getDeviceQueueCount() const { return mProperties.queueCount; } private: VkQueueFamilyProperties mProperties; uint32_t mQueueFamilyIndex; }; class DeviceQueueMap final { public: DeviceQueueMap() : mQueueFamilyIndex(QueueFamily::kInvalidIndex), mIsProtected(false) {} ~DeviceQueueMap(); void initialize(VkDevice device, const QueueFamily &queueFamily, bool makeProtected, uint32_t queueIndex, uint32_t queueCount); void destroy(); bool valid() const { return (mQueueFamilyIndex != QueueFamily::kInvalidIndex); } uint32_t getQueueFamilyIndex() const { return mQueueFamilyIndex; } bool isProtected() const { return mIsProtected; } egl::ContextPriority getDevicePriority(egl::ContextPriority priority) const { return mQueueAndIndices[priority].devicePriority; } DeviceQueueIndex getDeviceQueueIndex(egl::ContextPriority priority) const { return DeviceQueueIndex(mQueueFamilyIndex, mQueueAndIndices[priority].index); } const VkQueue &getQueue(egl::ContextPriority priority) const { return mQueueAndIndices[priority].queue; } // Wait for all queues to be idle, called on device loss and destruction. void waitAllQueuesIdle(); private: uint32_t mQueueFamilyIndex; bool mIsProtected; struct QueueAndIndex { // The actual priority that used egl::ContextPriority devicePriority; VkQueue queue; // The queueIndex used for VkGetDeviceQueue uint32_t index; }; angle::PackedEnumMap mQueueAndIndices; }; class CommandPoolAccess : angle::NonCopyable { public: CommandPoolAccess(); ~CommandPoolAccess(); angle::Result initCommandPool(ErrorContext *context, ProtectionType protectionType, const uint32_t queueFamilyIndex); void destroy(VkDevice device); void destroyPrimaryCommandBuffer(VkDevice device, PrimaryCommandBuffer *primaryCommands) const; angle::Result collectPrimaryCommandBuffer(ErrorContext *context, const ProtectionType protectionType, PrimaryCommandBuffer *primaryCommands); angle::Result flushOutsideRPCommands(Context *context, ProtectionType protectionType, egl::ContextPriority priority, OutsideRenderPassCommandBufferHelper **outsideRPCommands); angle::Result flushRenderPassCommands(Context *context, const ProtectionType &protectionType, const egl::ContextPriority &priority, const RenderPass &renderPass, VkFramebuffer framebufferOverride, RenderPassCommandBufferHelper **renderPassCommands); void flushWaitSemaphores(ProtectionType protectionType, egl::ContextPriority priority, std::vector &&waitSemaphores, std::vector &&waitSemaphoreStageMasks); angle::Result getCommandsAndWaitSemaphores( ErrorContext *context, ProtectionType protectionType, egl::ContextPriority priority, CommandBatch *batchOut, std::vector &&imagesToTransitionToForeign, std::vector *waitSemaphoresOut, std::vector *waitSemaphoreStageMasksOut); private: angle::Result ensurePrimaryCommandBufferValidLocked(ErrorContext *context, const ProtectionType &protectionType, const egl::ContextPriority &priority) { CommandsState &state = mCommandsStateMap[priority][protectionType]; if (state.primaryCommands.valid()) { return angle::Result::Continue; } ANGLE_TRY(mPrimaryCommandPoolMap[protectionType].allocate(context, &state.primaryCommands)); VkCommandBufferBeginInfo beginInfo = {}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; beginInfo.pInheritanceInfo = nullptr; ANGLE_VK_TRY(context, state.primaryCommands.begin(beginInfo)); return angle::Result::Continue; } // This mutex ensures vulkan command pool is externally synchronized. // This means no two threads are operating on command buffers allocated from // the same command pool at the same time. The operations that this mutex // protect include: // 1) recording commands on any command buffers allocated from the same command pool // 2) allocate, free, reset command buffers from the same command pool. // 3) any operations on the command pool itself mutable angle::SimpleMutex mCmdPoolMutex; using PrimaryCommandPoolMap = angle::PackedEnumMap; using CommandsStateMap = angle::PackedEnumMap>; CommandsStateMap mCommandsStateMap; // Keeps a free list of reusable primary command buffers. PrimaryCommandPoolMap mPrimaryCommandPoolMap; }; // Note all public APIs of CommandQueue class must be thread safe. class CommandQueue : angle::NonCopyable { public: CommandQueue(); ~CommandQueue(); angle::Result init(ErrorContext *context, const QueueFamily &queueFamily, bool enableProtectedContent, uint32_t queueCount); void destroy(ErrorContext *context); void handleDeviceLost(Renderer *renderer); // These public APIs are inherently thread safe. Thread unsafe methods must be protected methods // that are only accessed via ThreadSafeCommandQueue API. egl::ContextPriority getDriverPriority(egl::ContextPriority priority) const { return mQueueMap.getDevicePriority(priority); } DeviceQueueIndex getDeviceQueueIndex(egl::ContextPriority priority) const { return mQueueMap.getDeviceQueueIndex(priority); } VkQueue getQueue(egl::ContextPriority priority) const { return mQueueMap.getQueue(priority); } // The following are used to implement EGL_ANGLE_device_vulkan, and are called by the // application when it wants to access the VkQueue previously retrieved from ANGLE. Do not call // these for synchronization within ANGLE. void lockVulkanQueueForExternalAccess() { mQueueSubmitMutex.lock(); } void unlockVulkanQueueForExternalAccess() { mQueueSubmitMutex.unlock(); } Serial getLastSubmittedSerial(SerialIndex index) const { return mLastSubmittedSerials[index]; } // The ResourceUse still have unfinished queue serial by ANGLE or vulkan. bool hasResourceUseFinished(const ResourceUse &use) const { return use <= mLastCompletedSerials; } bool hasQueueSerialFinished(const QueueSerial &queueSerial) const { return queueSerial <= mLastCompletedSerials; } // The ResourceUse still have queue serial not yet submitted to vulkan. bool hasResourceUseSubmitted(const ResourceUse &use) const { return use <= mLastSubmittedSerials; } bool hasQueueSerialSubmitted(const QueueSerial &queueSerial) const { return queueSerial <= mLastSubmittedSerials; } // Wait until the desired serial has been completed. angle::Result finishResourceUse(ErrorContext *context, const ResourceUse &use, uint64_t timeout); angle::Result finishQueueSerial(ErrorContext *context, const QueueSerial &queueSerial, uint64_t timeout); angle::Result waitIdle(ErrorContext *context, uint64_t timeout); angle::Result waitForResourceUseToFinishWithUserTimeout(ErrorContext *context, const ResourceUse &use, uint64_t timeout, VkResult *result); bool isBusy(Renderer *renderer) const; angle::Result submitCommands(ErrorContext *context, ProtectionType protectionType, egl::ContextPriority priority, VkSemaphore signalSemaphore, SharedExternalFence &&externalFence, std::vector &&imagesToTransitionToForeign, const QueueSerial &submitQueueSerial); angle::Result queueSubmitOneOff(ErrorContext *context, ProtectionType protectionType, egl::ContextPriority contextPriority, VkCommandBuffer commandBufferHandle, VkSemaphore waitSemaphore, VkPipelineStageFlags waitSemaphoreStageMask, const QueueSerial &submitQueueSerial); // Note: Some errors from present are not fatal. VkResult queuePresent(egl::ContextPriority contextPriority, const VkPresentInfoKHR &presentInfo); angle::Result checkCompletedCommands(ErrorContext *context) { std::lock_guard lock(mCmdCompleteMutex); return checkCompletedCommandsLocked(context); } bool hasFinishedCommands() const { return !mFinishedCommandBatches.empty(); } angle::Result checkAndCleanupCompletedCommands(ErrorContext *context) { ANGLE_TRY(checkCompletedCommands(context)); if (!mFinishedCommandBatches.empty()) { ANGLE_TRY(releaseFinishedCommandsAndCleanupGarbage(context)); } return angle::Result::Continue; } ANGLE_INLINE void flushWaitSemaphores( ProtectionType protectionType, egl::ContextPriority priority, std::vector &&waitSemaphores, std::vector &&waitSemaphoreStageMasks) { return mCommandPoolAccess.flushWaitSemaphores(protectionType, priority, std::move(waitSemaphores), std::move(waitSemaphoreStageMasks)); } ANGLE_INLINE angle::Result flushOutsideRPCommands( Context *context, ProtectionType protectionType, egl::ContextPriority priority, OutsideRenderPassCommandBufferHelper **outsideRPCommands) { return mCommandPoolAccess.flushOutsideRPCommands(context, protectionType, priority, outsideRPCommands); } ANGLE_INLINE angle::Result flushRenderPassCommands( Context *context, ProtectionType protectionType, const egl::ContextPriority &priority, const RenderPass &renderPass, VkFramebuffer framebufferOverride, RenderPassCommandBufferHelper **renderPassCommands) { return mCommandPoolAccess.flushRenderPassCommands( context, protectionType, priority, renderPass, framebufferOverride, renderPassCommands); } const angle::VulkanPerfCounters getPerfCounters() const; void resetPerFramePerfCounters(); // Release finished commands and clean up garbage immediately, or request async clean up if // enabled. angle::Result releaseFinishedCommandsAndCleanupGarbage(ErrorContext *context); angle::Result releaseFinishedCommands(ErrorContext *context) { std::lock_guard lock(mCmdReleaseMutex); return releaseFinishedCommandsLocked(context); } angle::Result postSubmitCheck(ErrorContext *context); bool isInFlightCommandsEmpty() const; // Try to cleanup garbage and return if something was cleaned. Otherwise, wait for the // mInFlightCommands and retry. angle::Result cleanupSomeGarbage(ErrorContext *context, size_t minInFlightBatchesToKeep, bool *anyGarbageCleanedOut); // All these private APIs are called with mutex locked, so we must not take lock again. private: // Check the first command buffer in mInFlightCommands and update mLastCompletedSerials if // finished angle::Result checkOneCommandBatchLocked(ErrorContext *context, bool *finished); // Similar to checkOneCommandBatch, except we will wait for it to finish angle::Result finishOneCommandBatch(ErrorContext *context, uint64_t timeout, std::unique_lock *lock); void onCommandBatchFinishedLocked(CommandBatch &&batch); // Walk mFinishedCommands, reset and recycle all command buffers. angle::Result releaseFinishedCommandsLocked(ErrorContext *context); // Walk mInFlightCommands, check and update mLastCompletedSerials for all commands that are // finished angle::Result checkCompletedCommandsLocked(ErrorContext *context); angle::Result queueSubmitLocked(ErrorContext *context, egl::ContextPriority contextPriority, const VkSubmitInfo &submitInfo, DeviceScoped &commandBatch, const QueueSerial &submitQueueSerial); void pushInFlightBatchLocked(CommandBatch &&batch); void moveInFlightBatchToFinishedQueueLocked(CommandBatch &&batch); void popFinishedBatchLocked(); void popInFlightBatchLocked(); CommandPoolAccess mCommandPoolAccess; // Warning: Mutexes must be locked in the order as declared below. // Protect multi-thread access to mInFlightCommands.push/back and ensure ordering of submission. // Also protects mPerfCounters. mutable angle::SimpleMutex mQueueSubmitMutex; // Protect multi-thread access to mInFlightCommands.pop/front and // mFinishedCommandBatches.push/back. angle::SimpleMutex mCmdCompleteMutex; // Protect multi-thread access to mFinishedCommandBatches.pop/front. angle::SimpleMutex mCmdReleaseMutex; CommandBatchQueue mInFlightCommands; // Temporary storage for finished command batches that should be reset. CommandBatchQueue mFinishedCommandBatches; // Combined number of batches in mInFlightCommands and mFinishedCommandBatches queues. // Used instead of calculating the sum because doing this is not thread safe and will require // the mCmdCompleteMutex lock. std::atomic_size_t mNumAllCommands; // Queue serial management. AtomicQueueSerialFixedArray mLastSubmittedSerials; // This queue serial can be read/write from different threads, so we need to use atomic // operations to access the underlying value. Since we only do load/store on this value, it // should be just a normal uint64_t load/store on most platforms. AtomicQueueSerialFixedArray mLastCompletedSerials; // QueueMap DeviceQueueMap mQueueMap; FenceRecycler mFenceRecycler; angle::VulkanPerfCounters mPerfCounters; }; ANGLE_INLINE bool CommandQueue::isInFlightCommandsEmpty() const { return mInFlightCommands.empty(); } // A helper thread used to clean up garbage class CleanUpThread : public ErrorContext { public: CleanUpThread(Renderer *renderer, CommandQueue *commandQueue); ~CleanUpThread() override; // Context void handleError(VkResult result, const char *file, const char *function, unsigned int line) override; angle::Result init(); void destroy(ErrorContext *context); void requestCleanUp(); std::thread::id getThreadId() const { return mTaskThread.get_id(); } private: bool hasPendingError() const { std::lock_guard queueLock(mErrorMutex); return !mErrors.empty(); } angle::Result checkAndPopPendingError(ErrorContext *errorHandlingContext); // Entry point for clean up thread, calls processTasksImpl to do the // work. called by Renderer::initializeDevice on main thread void processTasks(); // Clean up thread, called by processTasks. The loop waits for work to // be submitted from a separate thread. angle::Result processTasksImpl(bool *exitThread); CommandQueue *const mCommandQueue; mutable angle::SimpleMutex mErrorMutex; std::queue mErrors; // Command queue worker thread. std::thread mTaskThread; bool mTaskThreadShouldExit; std::mutex mMutex; std::condition_variable mWorkAvailableCondition; std::atomic mNeedCleanUp; }; // Provides access to the PrimaryCommandBuffer while also locking the corresponding CommandPool class [[nodiscard]] ScopedPrimaryCommandBuffer final { public: explicit ScopedPrimaryCommandBuffer(VkDevice device) : mCommandBuffer(device) {} void assign(std::unique_lock &&poolLock, PrimaryCommandBuffer &&commandBuffer) { ASSERT(poolLock.owns_lock()); ASSERT(commandBuffer.valid()); ASSERT(mPoolLock.mutex() == nullptr); ASSERT(!mCommandBuffer.get().valid()); mPoolLock = std::move(poolLock); mCommandBuffer.get() = std::move(commandBuffer); } PrimaryCommandBuffer &get() { ASSERT(mPoolLock.owns_lock()); ASSERT(mCommandBuffer.get().valid()); return mCommandBuffer.get(); } DeviceScoped unlockAndRelease() { ASSERT(mCommandBuffer.get().valid() && mPoolLock.owns_lock() || !mCommandBuffer.get().valid() && mPoolLock.mutex() == nullptr); mPoolLock = {}; return std::move(mCommandBuffer); } private: std::unique_lock mPoolLock; DeviceScoped mCommandBuffer; }; } // namespace vk } // namespace rx #endif // LIBANGLE_RENDERER_VULKAN_COMMAND_QUEUE_H_