diff --git a/source_common/comms/comms_interface.hpp b/source_common/comms/comms_interface.hpp index 187bf31..0b6af17 100644 --- a/source_common/comms/comms_interface.hpp +++ b/source_common/comms/comms_interface.hpp @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: MIT * ---------------------------------------------------------------------------- - * Copyright (c) 2024 Arm Limited + * Copyright (c) 2024-2025 Arm Limited * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -63,7 +63,7 @@ static const EndpointID NO_ENDPOINT { 0 }; class CommsInterface { public: - virtual ~CommsInterface() { } + virtual ~CommsInterface() = default; /** * @brief Is this comms module connected to a host server? diff --git a/source_common/framework/manual_functions.cpp b/source_common/framework/manual_functions.cpp index bcae148..8358077 100644 --- a/source_common/framework/manual_functions.cpp +++ b/source_common/framework/manual_functions.cpp @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: MIT * ---------------------------------------------------------------------------- - * Copyright (c) 2024 Arm Limited + * Copyright (c) 2024-2025 Arm Limited * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -297,7 +297,8 @@ PFN_vkVoidFunction layer_vkGetInstanceProcAddr_default( // Otherwise, only expose functions that the driver exposes to avoid // changing queryable interface behavior seen by the application layerFunction = getInstanceLayerFunction(pName); - if (instance) { + if (instance) + { std::unique_lock lock { g_vulkanLock }; auto* layer = Instance::retrieve(instance); diff --git a/source_common/utils/async_lcs.cpp b/source_common/utils/async_lcs.cpp new file mode 100644 index 0000000..265c5f5 --- /dev/null +++ b/source_common/utils/async_lcs.cpp @@ -0,0 +1,161 @@ +/* + * SPDX-License-Identifier: MIT + * ---------------------------------------------------------------------------- + * Copyright (c) 2025 Arm Limited + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * ---------------------------------------------------------------------------- + */ + +/** + * @file + * Implementation of the asynchronous layer command stream. + */ + +#include +#include + +#include "utils/async_lcs.hpp" + +/* See header for documentation. */ +ALSCTaskTimelineSem::ALSCTaskTimelineSem( + Device& _device, + VkSemaphore _semaphore, + uint64_t _waitValue, + Action _action +): + device(_device), + semaphore(_semaphore), + waitValue(_waitValue), + action(_action) +{ + +} + +/* See header for documentation. */ +bool ALSCTaskTimelineSem::waitForRunnable() +{ + VkSemaphoreWaitInfo waitInfo { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, + .pNext = nullptr, + .flags = 0, + .semaphoreCount = 1, + .pSemaphores = &semaphore, + .pValues = &waitValue + }; + + auto result = vkWaitSemaphores(device.device, &waitInfo, UINT64_MAX); + return result == VK_SUCCESS; +} + +/* See header for documentation. */ +void ALSCTaskTimelineSem::runTask() +{ + action(); +} + +/* See header for documentation. */ +ALSCTaskEvent::ALSCTaskEvent( + Device& _device, + VkEvent _event, + Action _action +): + device(_device), + event(_event), + action(_action) +{ + +} + +/* See header for documentation. */ +bool ALSCTaskEvent::waitForRunnable() +{ + bool success { false }; + while(true) + { + auto result = vkGetEventStatus(device.device, event); + if (result == VK_EVENT_RESET) + { + std::this_thread::sleep_for(std::chrono::microseconds(100)); + continue; + } + + // Event is either set or Vulkan is in error ... + success = result == VK_EVENT_SET; + break; + } + + return success; +} + +/* See header for documentation. */ +void ALSCTaskEvent::runTask() +{ + action(); +} + +/* See header for documentation. */ +ALSCRunner::ALSCRunner() +{ + // Create and start a worker thread + worker = std::thread(&ALSCRunner::runWorker, this); +} + +/* See header for documentation. */ +void ALSCRunner::put( + std::shared_ptr task +) { + queue.put(task); +} + +/* See header for documentation. */ +void ALSCRunner::stop() +{ + // Put a stop event at the end of the queue + // All previous events are processed before stopping + std::shared_ptr stopTask = std::make_shared(); + put(stopTask); + + // Wait for the thread to complete pending events and shutdown + worker.join(); +} + +void ALSCRunner::runWorker() +{ + while (true) + { + auto event = queue.get(); + + // Event is a stop event used to wake the runner on shutdown + if (event->isStop()) + { + event->notify(); + break; + } + + // Event is a real event that needs handling + bool success = event->waitForRunnable(); + if (success) + { + event->runTask(); + } + + event->notify(); + } +} diff --git a/source_common/utils/async_lcs.hpp b/source_common/utils/async_lcs.hpp new file mode 100644 index 0000000..7c499c5 --- /dev/null +++ b/source_common/utils/async_lcs.hpp @@ -0,0 +1,296 @@ +/* + * SPDX-License-Identifier: MIT + * ---------------------------------------------------------------------------- + * Copyright (c) 2025 Arm Limited + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * ---------------------------------------------------------------------------- + */ + +/** + * @file + * Vulkan layers often need to perform tasks that can only be performed when + * submitted GPU command streams have progressed to a certain point. Triggers + * for CPU-side work can include: + * + * - A timeline semaphore reaching a threshold value when a submit completes. + * - An event being signalled when a command stream reaches a certain point*. + * + * (*): Waiting on an event that is set by a command stream while its submit + * is still executing, and using the triggered CPU-side work to set an event + * for GPU-side work that is already submitted is out-of-spec. However, some of + * our layers rely on it to avoid the need to have the layer manually split + * apart application command buffers. + * + * The asynchronous layer command stream (ALCS) is a way of quickly and + * reliably managing execution of CPU-side tasks that can only be triggered + * when a particular milestone is reached in the command stream. To allow these + * async tasks to progress independently from the application API calls, the + * async layer command stream manager creates one thread per Vulkan queue to + * perform all async tasks associated with that queue. + * + * Cross-queue async work can be implemented by using Timeline semaphores and + * injecting the ALSC task into the queue that writes the highest timeline + * semaphore value. Cross-queue handling is implemented outside of the ALSC + * module. + * + * + * Async trigger: Timeline semaphore + * --------------------------------- + * + * Work associated with a timeline semaphore must also specify the time value + * to wait for, and will block and wait for the semaphore to reach that value. + * + * Async trigger: Event + * -------------------- + * + * Work associated with a binary event will block and wait for the binary + * event to be signalled. + * + * Threading model + * --------------- + * + * The ALSC workloads for any single queue are processed in FIFO order, with + * the worker thread dequeuing one task at a time and then blocking waiting for + * it to become eligible for execution. The layer MUST add ALSC workloads to + * the queue in the order they are signalled by the implementation or deadlock + * will occur. + * + * The ALSC callbacks that run to execute an async task and triggered with no + * locks held. The layer MUST use appropriate locking in the event handler + * is accessing shared data resources. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include "device.hpp" // Include from per-layer code +#include "utils/queue.hpp" + +/** + * @brief Baseclass for an async layer command stream task. + */ +class ALSCTask : public Task +{ +public: + /** + * @brief Type for execution callbacks. + */ + using Action = std::function; + + /** + * @brief Destroy the task. + */ + virtual ~ALSCTask() = default; + + /** + * @brief Wait for this task to become runnable. + * + * @return True on success, or False on a Vulkan error. + */ + virtual bool waitForRunnable() = 0; + + /** + * @brief Run this task. + */ + virtual void runTask() = 0; + + /** + * @brief Is this a stop task? + */ + virtual bool isStop() + { + return false; + } +}; + +/** + * @brief Dummy task use to unblock the task queue. + */ +class ALSCTaskStop : public ALSCTask +{ +public: + /** + * @brief Destroy the task. + */ + virtual ~ALSCTaskStop() = default; + + /* See baseclass for documentation. */ + bool waitForRunnable() + { + return true; + } + + /* See baseclass for documentation. */ + virtual void runTask() { }; + + /* See baseclass for documentation. */ + virtual bool isStop() + { + return true; + } +}; + +/** + * @brief An async layer command stream task triggered by a timeline sem. + */ +class ALSCTaskTimelineSem : public ALSCTask +{ +public: + /** + * @brief Create a new async event. + * + * @param device The layer device context. + * @param semaphore The triggering timeline semaphore. + * @param waitValue The target timeline value to wait for. + * @param action The callback to trigger. + */ + ALSCTaskTimelineSem( + Device& device, + VkSemaphore semaphore, + uint64_t waitValue, + Action action); + + /** + * @brief Destroy the task. + */ + virtual ~ALSCTaskTimelineSem() = default; + + /* See baseclass for documentation. */ + virtual bool waitForRunnable(); + + /* See baseclass for documentation. */ + virtual void runTask() ; + +private: + /** + * @brief The layer device context. + */ + Device& device; + + /** + * @brief The layer semaphore to wait for. + */ + VkSemaphore semaphore; + + /** + * @brief The layer timeline value to wait for. + */ + uint64_t waitValue; + + /** + * @brief The action to trigger. + */ + Action action; +}; + +/** + * @brief An async layer command stream task triggered by an event. + */ +class ALSCTaskEvent : public ALSCTask +{ +public: + /** + * @brief Create a new async event. + * + * @param device The layer device context. + * @param event The triggering event. + * @param action The callback to trigger. + */ + ALSCTaskEvent( + Device& device, + VkEvent event, + Action action); + + /* See baseclass for documentation. */ + virtual ~ALSCTaskEvent() = default; + + /* See baseclass for documentation. */ + virtual bool waitForRunnable(); + + /* See baseclass for documentation. */ + virtual void runTask(); + +private: + /** + * @brief The layer device context. + */ + Device& device; + + /** + * @brief The layer event to wait for. + */ + VkEvent event; + + /** + * @brief The action to trigger. + */ + Action action; +}; + + +/** + * @brief Runner class used to run the async command stream. + * + * Commands in the command stream are executed in FIFO order. Queue commands in + * such a way to avoid deadlocks. + */ +class ALSCRunner +{ +public: + /** + * @brief Construct an async queue and start the runner working. + */ + ALSCRunner(); + + /** + * @brief Put a new async task on the end of the queue. + * + * @param task The async task to execute when it becomes runnable. + */ + void put( + std::shared_ptr task); + + /** + * @brief Stop the runner thread and wait for it to stop. + */ + void stop(); + + /** + * @brief Async entrypoint for the worker thread. + */ + void runWorker(); + +private: + /** + * @brief The worker thread running the async handler. + */ + std::thread worker; + + /** + * @brief The FIFO task queue of async tasks. + */ + TaskQueue> queue; +}; diff --git a/source_common/utils/promise.hpp b/source_common/utils/promise.hpp new file mode 100644 index 0000000..42441bc --- /dev/null +++ b/source_common/utils/promise.hpp @@ -0,0 +1,212 @@ +/* + * SPDX-License-Identifier: MIT + * ---------------------------------------------------------------------------- + * Copyright (c) 2014-2025 Arm Limited + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * ---------------------------------------------------------------------------- + */ + +/** + * @file + * This module implements a way of wrapping arbitrary resource cleanup for + * resources that are not natively RAII-managed. + */ + +#pragma once + +#include +#include +#include + +/** + * @brief RAII managed trigger for cleanup actions. + * + * A Promise is designed as a means to manage arbitrary cleanup actions using + * C++ object lifetime to trigger cleanup when the Promise is destroyed. + * Typically a Promise will be allocated on the stack, executing cleanups when + * the object drops out of scope. + * + * - A Promise can be manually resolved early by calling fulfill(). + * - A Promise can be discarded without executing by calling dispose(). + * - A Promise cannot be copy constructed or copy assigned. + * - A Promise can be move constructed or move assigned. + * + * All references to the resource itself are via the "action" object, which + * will typically be a dynamically created lambda function. + */ +class Promise +{ +public: + /* Disable copying; only moving allowed. */ + Promise(const Promise&) = delete; + Promise& operator=(const Promise&) = delete; + + /** @brief Resource cleanup is a "void func()" function. */ + using Action = std::function; + + /** + * @brief Create a new promise. + * + * @param[in] action A callable action to execute in future. + */ + Promise(const Action& action) : action(action) { } + + /** + * @brief Destroy the promise. + * + * This will execute the promised action, unless it has already been + * manually resolved or discarded. + */ + ~Promise() + { + fulfill(); + } + + /** + * @brief Create a new promise by moving the action out of another. + * + * @param[in] that The promise to move the action from. + */ + Promise(Promise&& that) : action(nullptr) + { + std::swap(this->action, that.action); + } + + /** + * @brief Assign a new promise by moving the action out of another. + * + * @param[in] that The promise to move the action from. + */ + Promise& operator=(Promise&& that) + { + Promise temp(std::move(that)); + std::swap(this->action, temp.action); + return *this; + } + + /** + * @brief Fullfill the promise. + */ + void fulfill() + { + // Swap with nullptr so that calling fulfill twice is benign + Action action_copy(nullptr); + std::swap(action, action_copy); + if (action_copy != nullptr) + { + action_copy(); + } + } + + /** + * @brief Dispose of the promise without executing it. + */ + void dispose() + { + action = nullptr; + } + +private: + /** @brief The stored action function. */ + Action action; +}; + +/** + * @brief RAII managed trigger for a stack of cleanup actions. + * + * Promises are designed as a means to manage arbitrary cleanup actions stored + * in a Promise, using the Promises container object lifetime to trigger + * cleanup of the Promise objects it contains when the container is destroyed. + * + * Promise actions in this container are fulfilled in the reverse order to + * which they were added, i.e. LIFO, which is often needed for resource + * cleanup actions. + * + * - A Promises stack can be resolved early by calling fulfill(). + * - A Promises stack cannot be copy constructed or copy assigned. + * - A Promises stack can be move constructed or move assigned. + */ +class Promises +{ +public: + /* Disable all copies and moves of the container type. */ + Promises(const Promise&) = delete; + Promises& operator=(const Promise&) = delete; + Promises(Promises &&) = delete; + Promises& operator= (Promises &&) = delete; + + /** + * @brief Create a new empty stack. + */ + Promises() = default; + + /** + * @brief Destroy the promises stack. + * + * This will fulfill all stored promises in LIFO order. + */ + ~Promises() + { + fulfill(); + } + + /** + * @brief Add a new clean up action to the list. + * + * @param action A callable action to execute in future. + */ + void add(const Promise::Action & action) + { + promises.emplace_back(action); + } + + /** + * @brief Add a new promise to the list. + * + * The original Promise passed in will be invalid after this, as ownership + * of the stored action it contained will have been transferred to another + * Promise owned by this container. + * + * @param promise A previously created Promised action. + */ + void add(Promise && promise) + { + promises.emplace_back(std::move(promise)); + } + + /** + * @brief Fulfill all stored promises in LIFO order. + */ + void fulfill() + { + // Move to clear the stored list + std::vector promises_copy(std::move(promises)); + + // Resolve the promises in reverse order + for(auto& i : std::ranges::reverse_view(promises_copy)) + { + i.fulfill(); + } + } + +private: + /** @brief The stored list of promises. */ + std::vector promises; +}; diff --git a/source_common/utils/queue.hpp b/source_common/utils/queue.hpp index 4434837..373f68d 100644 --- a/source_common/utils/queue.hpp +++ b/source_common/utils/queue.hpp @@ -1,7 +1,7 @@ /* * SPDX-License-Identifier: MIT * ---------------------------------------------------------------------------- - * Copyright (c) 2024 Arm Limited + * Copyright (c) 2024-2025 Arm Limited * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -44,12 +44,13 @@ class Task /** * @brief Destroy the task. */ - virtual ~Task() { } + virtual ~Task() = default; /** * @brief Wait for the task to be complete. */ - void wait() { + void wait() + { std::unique_lock lock(condition_lock); complete_condition.wait(lock, [this]{ return complete.load(); }); } @@ -57,7 +58,8 @@ class Task /** * @brief Notify that the task is complete. */ - void notify() { + void notify() + { std::unique_lock lock(condition_lock); complete = true; lock.unlock();