diff --git a/.gitignore b/.gitignore index 41153d3..0ee6559 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,12 @@ compile_commands.json .cache logs/ CMakeFiles/ - +latex +html # macOS .DS_Store - +.idea + # i was trying jet brains lol # vcpkg vcpkg/buildtrees/ vcpkg/downloads/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 4877d88..bd891c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,8 +11,9 @@ include(QuarkOptions) find_package(Vulkan REQUIRED) if(NOT Vulkan_FOUND) - message(FATAL_ERROR - "Vulkan SDK/loader was not found. If you use vcpkg, ensure the vcpkg submodule is initialised and bootstrapped (make deps) and that dependencies from vcpkg.json are installed for the active triplet." + message( + FATAL_ERROR + "Vulkan SDK/loader was not found. If you use vcpkg, ensure the vcpkg submodule is initialised and bootstrapped (make deps) and that dependencies from vcpkg.json are installed for the active triplet." ) endif() diff --git a/docs/rfc/RFC-0002.md b/docs/rfc/RFC-0002.md new file mode 100644 index 0000000..a038802 --- /dev/null +++ b/docs/rfc/RFC-0002.md @@ -0,0 +1,124 @@ +# RFC-0001 + +**Status:** Draft +**Author:** +**Target:** Internal Engine Architecture +**Date:** March, 2026 + +## 1. Abstract + +## 2. Motivation + +## 3. Design Principles + +The following principles guide this design: + +### 3.1 Shape over semantics + +Concepts enforce type shape and API presence. + +### 3.2 Minimal Sufficiency + +Concepts should require only what is necessary for correctness and interoperability. + +### 3.3 Non-intrusiveness + +Concepts must not impose unnecessary contraints on layout, +performance, or implementation strategy. + +## 4 Concepts + +### 4.1 MoveOnlyNoexcept + +#### 4.1.1 Definition + +A type satisfying MoveOnlyNoexcept: + +- Is moveable +- Is not copyable +- Has noexcept move construction and assignment + +#### 4.1.2 Rationale + +Registries and bundles represent ownership domains and must: + +- Avoid accidental copying +- Be safely relocatable + +Noexcept move is required to preserve strong exception safety +guarantees in higher-level systems. + +#### 4.1.3 Specification + +```cpp +template +concept MoveOnlyNoexcept = + std::moveable && + !std::copy_constructible && + !std::copy_assignable_v && + std::is_nothrow_move_constructible_v && + std::is_nothrow_move_assignable_v; +``` + +### 4.2 RegistrySlot + +#### 4.2.1 Definition + +A type satisfying RegistrySlot: + +- Tracks whether the slot is live +- Tracks the current generation of the slot + +#### 4.2.2 Rationale + +Slots represent storage locations for registry managed objects. +They must: + +- Distinguish between live and free states +- Participate in generation-based validation + +This enables: + +- Detection of stale handles +- Safe reuse of slot indices + +#### 4.2.3 Specification + +```cpp +template +concept RegistrySlot = + requires(S slot) { + { slot.live } -> std::convertible_to; + { slot.generation } -> std::convertible_to; + }; +``` + +Layer 1: common concepts + • BundleLike + +The following are explicitly out of scope for these concepts: + • Enforcing transactional creation + • Enforcing correct retirement semantics + • Preventing logical misuse of handles + • Guaranteeing destruction ordering + • Enforcing trivial move or standard layout + +Layer 2: registry CRTP base + +Responsible for: + • retire_queue_ + • slots_ + • free_ + • alive() + • allocate_handle_() + • get_slot_if_live_() + • retire_slot_() + • clear() + +Layer 3: concrete registry + +FrameRegistry implements: + • create() + • make_retired_payload_() + • retired payload destroy/cleanup trampolines + • frame-specific accessors diff --git a/include/quark/engine/retire/retirement_queue.hpp b/include/quark/engine/retire/retirement_queue.hpp new file mode 100644 index 0000000..d127ea4 --- /dev/null +++ b/include/quark/engine/retire/retirement_queue.hpp @@ -0,0 +1,122 @@ +#pragma once + +// TODO: replace with gpu timeline interface i.e. abstracted from vulkan +#include "quark/vk/sync/gpu_timeline.hpp" + +#include +#include +#include +#include +#include + +namespace quark::vk { + +/** + * @brief CPU-side deferred destruction queue keyed by a GPU timeline value. + * + * Each entry is scheduled with a retire value (retire_at) on a global timeline. + * It is safe to run the entry once completed_value() >= retire_at. + * + * Performance: + * - enqueue: O(1) + * - drain: O(k) where k is the number of ready items drained + * + * Invariant: + * - Internally maintains a monotonic retire_at sequence. + * If the caller enqueues an out-of-order retire_at, it is clamped + * to the last enqueued retire_at to preserve monotonicity and allow O(k) + * front-drain. + * + * Correctness assumptions: + * - All retire_at values refer to the same total order clock as timeline_. + * - destroy() is called only when it is safe to execute all pending callbacks + * (e.g. after vkDeviceWaitIdle()). + */ +class RetirementQueue final { +public: + /** + * @brief Type-erased task invoked on retirement. + * + * fn(ctx) is executed when the entry is drained. + * cleanup(ctx) is executed afterwards to free ctx memory. + */ + struct Task { + void (*fn)(void *ctx) noexcept = nullptr; + void (*cleanup)(void *ctx) noexcept = nullptr; + void *ctx = nullptr; + }; + + struct CreateInfo { + const GpuTimeline *timeline = nullptr; + + /// Optional; pre-reserve capacity + std::size_t reserve = 0; + }; + + RetirementQueue() = default; + ~RetirementQueue() { destroy(); } + + QUARK_MOVE_ONLY(RetirementQueue); + + [[nodiscard]] util::Status create(const CreateInfo &ci); + void destroy() noexcept; + + [[nodiscard]] bool valid() const noexcept { return timeline_ != nullptr; } + + /** + * @brief Enqueue a task to run after retire_at is completed. + * + * Fast-path: + * - If retire_at <= timeline.completed_value(), task executed immediately. + * + * Ordering: + * - If retire_at < last_enqueued_retire_at_, it is clamped upward to + * last_enqueued_retire_at_ to preserver monotonicity. + * + * @return Status::OK on Success, or OutOfMemory if internal growth fails. + */ + [[nodiscard]] util::Status enqueue(uint64_t retire_at, Task task); + + /** + * @brief Drain all ready tasks (completed_value >= retire_at). + * + * Runs tasks in FIFO order for the ready prefix. + * Does not block; it only checks timeline completed. + * + * @return the number of tasks executed. + */ + [[nodiscard]] util::Result drain() noexcept; + + /** + * @brief Number of tasks currently queued (including the ones not yet ready). + */ + [[nodiscard]] std::size_t pending() const noexcept { + return (entries_.size() >= head_) ? (entries_.size() - head_) : 0; + } + +private: + struct Entry { + uint64_t retire_at = 0; + Task task{}; + }; + + static void run_task_(Task &t) noexcept; + + /** + * @brief Avoid O(n) erase every frame by compacting when head is large + * enough. + * + * Two Part Heuristic: + * 1) dead >= live, i.e. at least half the buffer is garbage, OR + * 2) dead is large enough to matter in absolute terms + */ + void compact_if_needed_() noexcept; + + const GpuTimeline *timeline_ = nullptr; // non-owning + + std::vector entries_; + std::size_t head_ = 0; + uint64_t last_enqueued_retire_at_ = 0; +}; + +} // namespace quark::vk diff --git a/include/quark/utils/diagnostic.hpp b/include/quark/utils/diagnostic.hpp index f09dae8..0c5cb3c 100644 --- a/include/quark/utils/diagnostic.hpp +++ b/include/quark/utils/diagnostic.hpp @@ -27,6 +27,34 @@ inline void report_if_error(const util::Result &r) noexcept { #include +/** + * @def QUARK_TRY_STATUS(expr) + * @brief Propagates a failing util::Status (util::Result) to the caller. + * + * Evaluates @p expr exactly once and expects it to return util::Status + * (i.e. util::Result) or another util::Result like type with: + * - boolean conversion indicating success + * - .error() yielding util::Error + * + * On failure, returns util::unexpected(error) from the *current function*, + * transferring ownership of the error via move. + * + * Typical use: + * @code + * util::Status f() { + * QUARK_TRY_STATUS(g()); + * QUARK_OK(); + * } + * @endcode + * + * Requirements: + * 0 Must be used inside a function that returns util::Status (or compatible + * util::Result with matching error type). + * + * Notes: + * - This macro does not modify the error's source_location; the error's + * where-field remains whatever the callee produced. + */ #define QUARK_TRY_STATUS(expr) \ do { \ auto _q_res = (expr); \ @@ -35,6 +63,34 @@ inline void report_if_error(const util::Result &r) noexcept { } \ } while (0) +/** + * @def QUARK_TRY_ASSIGN(lhs, expr) + * @brief Propagates failure; otherwise assigns the success value to @p lhs. + * + * Evaluates @p expr exactly once. @p expr must return util::Result (or + * compatible) with: + * - boolean conversion indicating success + * - .value() returning T + * - .error() returning util::Error + * + * If @p expr fails, returns util::unexpected(error) from the current function. + * If it succeeds, moves the value into @p lhs. + * + * Example: + * @code + * util::Result make(); + * util::Status f() { + * int x = 0; + * QUARK_TRY_ASSIGN(x, make()); + * // x initialized on success + * QUARK_OK(); + * } + * @endcode + * + * Requirements: + * - @p lhs must be assignable from the result value type. + * - Current function must return a compatible util::Result<...>. + */ #define QUARK_TRY_ASSIGN(lhs, expr) \ do { \ auto _q_res = (expr); \ @@ -44,11 +100,89 @@ inline void report_if_error(const util::Result &r) noexcept { (lhs) = std::move(_q_res.value()); \ } while (0) +/** + * @def QUARK_TRY_VALIDATE(expr) + * @brief Propagates failure like QUARK_TRY_STATUS, but rewrites source + * location. + * + * This is intended specifically for validation style helper functions where + * the error is *structurally correct* but the most actionable source location + * is the call-site of validation, not the internal validation check. + * + * Behavior: + * - Evaluates @p expr exactly once. + * - On success: no-op. + * - On failure: + * - takes ownership of the error + * - appends a note containing the original failure location + * - overwrites error.where with std::source_location::current() + * - returns util::unexpected(modified_error) from the current function + * + * This yields logs like: + * - where = call-site of validate(...) + * - msg includes "(validate failed at :)" showing the original + * check + * + * Requirements: + * - @p expr must return util::Status or util::Result-like. + * - Current function must return util::Status (or compatible + * util::Result<...>). + * + * Notes: + * - This macro formats the augmented message using fmt::format; it therefore + * requires (directly or indirectly) to be included. + * - Use this macro only for validation entrypoints; do not use it for general + * error propagation or one will lose precise origin information. + */ +#define QUARK_TRY_VALIDATE(expr) \ + do { \ + auto _q_res = (expr); \ + if (!_q_res) { \ + auto _q_err = std::move(_q_res.error()); \ + _q_err.msg = fmt::format("{} (validate failed at {}:{})", _q_err.msg, \ + _q_err.where.file_name(), _q_err.where.line()); \ + _q_err.where = std::source_location::current(); \ + return ::util::unexpected(std::move(_q_err)); \ + } \ + } while (0) + +/** + * @def QUARK_FAIL(err_expr) + * @brief Returns util::unexpected(err_expr) from the current function. + * + * @p err_expr must produce a util::Error. + * + * Example: + * @code + * QUARK_FAIL(QUARK_ERR(util::Errc::InvalidArg, "x must be positive")); + * @endcode + * + * Requirements: + * - Current function must return util::Status or compatible util::Result. + */ #define QUARK_FAIL(err_expr) \ do { \ return ::util::unexpected((err_expr)); \ } while (0) +/** + * @def QUARK_ENSURE(cond, err_expr) + * @brief Enforces a condition; returns an error if the condition fails. + * + * If @p cond is false, returns util::unexpected(err_expr) from the current + * function. Otherwise continues. + * + * @p err_expr must produce a util::Error. + * + * Example: + * @code + * QUARK_ENSURE(ptr != nullptr, + * QUARK_ERR(util::Errc::InvalidArg, "ptr is null")); + * @endcode + * + * Requirements: + * - Current function must return util::Status or compatible util::Result. + */ #define QUARK_ENSURE(cond, err_expr) \ do { \ if (!(cond)) { \ @@ -56,6 +190,33 @@ inline void report_if_error(const util::Result &r) noexcept { } \ } while (0) +/** + * @def QUARK_OK() + * @brief Convenience return for successful util::Status. + * + * Equivalent to: + * @code + * return util::Status{}; + * @endcode + * + * Use to avoid repetitive `return {};` at the end of util::Status functions. + * + * I like rust. + */ +#define QUARK_OK() \ + return ::util::Status {} + +/** + * @def QUARK_LOG_INFO(fmtstr, ...) + * @brief Emits an informational diagnostic event. + * + * Formats @p fmtstr with fmt (via make_event) and reports it to the configured + * diagnostic sinks. The event source location is + * std::source_location::current() at the call site. + * + * @param fmtstr A fmt-compatible format string. + * @param ... Optional fmt arguments. + */ #define QUARK_LOG_INFO(fmtstr, ...) \ do { \ ::util::report(::util::details::make_event( \ @@ -63,6 +224,17 @@ inline void report_if_error(const util::Result &r) noexcept { (fmtstr)__VA_OPT__(, ) __VA_ARGS__)); \ } while (0) +/** \ + * @def QUARK_LOG_WARN(fmtstr, ...) \ + * @brief Emits a warning diagnostic event. \ + * \ + * Formats @p fmtstr with fmt (via make_event) and reports it to the \ + * configured diagnostic sinks. The event source location is \ + * std::source_location::current() at the call site. \ + * \ + * @param fmtstr A fmt-compatible format string. \ + * @param ... Optional fmt arguments. \ + */ \ #define QUARK_LOG_WARN(fmtstr, ...) \ do { \ ::util::report(::util::details::make_event( \ @@ -70,6 +242,14 @@ inline void report_if_error(const util::Result &r) noexcept { (fmtstr)__VA_OPT__(, ) __VA_ARGS__)); \ } while (0) +/** + * @def QUARK_LOG_INFO_MOD(mod, fmtstr, ...) + * @brief Emits an informational diagnostic event with an explicit module tag. + * + * @param mod Module/category name (e.g. "vk.instance", "io.fs"). + * @param fmtstr A fmt-compatible format string. + * @param ... Optional fmt arguments. + */ #define QUARK_LOG_INFO_MOD(mod, fmtstr, ...) \ do { \ ::util::report(::util::details::make_event( \ @@ -77,6 +257,14 @@ inline void report_if_error(const util::Result &r) noexcept { (fmtstr)__VA_OPT__(, ) __VA_ARGS__)); \ } while (0) +/** + * @def QUARK_LOG_WARN_MOD(mod, fmtstr, ...) + * @brief Emits a warning diagnostic event with an explicit module tag. + * + * @param mod Module/category name (e.g. "vk.instance", "io.fs"). + * @param fmtstr A fmt-compatible format string. + * @param ... Optional fmt arguments. + */ #define QUARK_LOG_WARN_MOD(mod, fmtstr, ...) \ do { \ ::util::report(::util::details::make_event( \ @@ -84,16 +272,66 @@ inline void report_if_error(const util::Result &r) noexcept { (fmtstr)__VA_OPT__(, ) __VA_ARGS__)); \ } while (0) +/** + * @def QUARK_ERR(code, fmtstr, ...) + * @brief Constructs a util::Error with severity Error and current source + * location. + * + * This does not report/log the error; it merely constructs it so it can be + * returned via util::unexpected(...) or otherwise handled. + * + * @param code Engine level error code (util::Errc). + * @param fmtstr A fmt compatible format string for the error message. + * @param ... Optional fmt arguments. + * + * @return util::Error with: + * - .code = @p code + * - .severity = util::Severity::Error + * - .module = empty (sink may infer from file) + * - .where = call-site source location + */ #define QUARK_ERR(code, fmtstr, ...) \ (::util::details::make_error((code), ::util::Severity::Error, {}, \ std::source_location::current(), \ (fmtstr)__VA_OPT__(, ) __VA_ARGS__)) +/** + * @def QUARK_ERR_MOD(code, mod, fmtstr, ...) + * @brief Constructs a util::Error with an explicit module tag. + * + * This does not report/log the error; it merely constructs it so it can be + * returned via util::unexpected(...) or otherwise handled. + * + * @param code Engine level error code (util::Errc). + * @param fmtstr A fmt compatible format string for the error message. + * @param ... Optional fmt arguments. + * + * @return util::Error with: + * - .code = @p code + * - .severity = util::Severity::Error + * - .module = empty (sink may infer from file) + * - .where = call-site source location + */ #define QUARK_ERR_MOD(code, mod, fmtstr, ...) \ (::util::details::make_error((code), ::util::Severity::Error, (mod), \ std::source_location::current(), \ (fmtstr)__VA_OPT__(, ) __VA_ARGS__)) +/** + * @def QUARK_REPORT_IF_ERROR(res_expr) + * @brief Evaluates a util::Result expression and reports the error if it + * failed. + * + * Evaluates @p res_expr exactly once. If it yields an error, the error is + * reported to diagnostic sinks. The result is not returned or propagated. + * + * Intended for best-effort cleanup paths where failures are non-fatal. + * + * Example: + * @code + * QUARK_REPORT_IF_ERROR(try_close_file()); + * @endcode + */ #define QUARK_REPORT_IF_ERROR(res_expr) \ do { \ auto _q_res = (res_expr); \ diff --git a/include/quark/utils/raii.hpp b/include/quark/utils/raii.hpp index a5c9109..2365b1d 100644 --- a/include/quark/utils/raii.hpp +++ b/include/quark/utils/raii.hpp @@ -1,21 +1,53 @@ #pragma once +/** + * @def QUARK_NO_COPY(Type) + * @brief Deletes copy constructor and copy assignment for @p Type. + * + * Use for RAII/handle-owning types to prevent accidental copying. + */ #define QUARK_NO_COPY(Type) \ Type(const Type &) = delete; \ Type &operator=(const Type &) = delete +/** + * @def QUARK_NO_MOVE(Type) + * @brief Deletes move constructor and move assignment for @p Type. + * + * Use when a type must not be moved (e.g. self-referential objects). + */ #define QUARK_NO_MOVE(Type) \ Type(Type &&) = delete; \ Type &operator=(Type &&) = delete +/** + * @def QUARK_NO_COPY_NO_MOVE(Type) + * @brief Deletes both copy and move operations for @p Type. + */ #define QUARK_NO_COPY_NO_MOVE(Type) \ QUARK_NO_COPY(Type); \ QUARK_NO_MOVE(Type) +/** + * @def QUARK_DEFAULT_MOVE(Type) + * @brief Defaults move constructor and move assignment (both noexcept). + * + * Only use when the type's members are safely movable and leaving the + * moved-from object in a destructible state is correct. + */ #define QUARK_DEFAULT_MOVE(Type) \ Type(Type &&) noexcept = default; \ Type &operator=(Type &&) noexcept = default +/** + * @def QUARK_MOVE_ONLY(Type) + * @brief Makes @p Type move-only: deletes copy operations and defaults noexcept + * move. + * + * Equivalent to: + * - QUARK_NO_COPY(Type) + * - QUARK_DEFAULT_MOVE(Type) + */ #define QUARK_MOVE_ONLY(Type) \ QUARK_NO_COPY(Type); \ QUARK_DEFAULT_MOVE(Type) diff --git a/include/quark/vk/device/details/device.hpp b/include/quark/vk/device/details/device.hpp index 051a867..961cb01 100644 --- a/include/quark/vk/device/details/device.hpp +++ b/include/quark/vk/device/details/device.hpp @@ -3,17 +3,23 @@ #include #include #include +#include #include #include -namespace quark::vk { +namespace quark::vk::details { class Device final { public: struct CreateInfo { VkInstance instance = VK_NULL_HANDLE; VkSurfaceKHR surface = VK_NULL_HANDLE; + const VkAllocationCallbacks *allocator = nullptr; + std::vector required_extensions; + + DeviceFeatureFlags required_features = 0; + DeviceFeatureFlags preferred_features = 0; }; Device() = default; @@ -50,13 +56,47 @@ class Device final { return present_queue_family_index_; } + [[nodiscard]] const DeviceCapabilities &capabilities() const noexcept { + return capabilities_; + } + private: + struct DeviceSelection { + VkPhysicalDevice physical_device{VK_NULL_HANDLE}; + uint32_t graphics_queue_family_index{0}; + uint32_t present_queue_family_index{0}; + }; + + static util::Result + pick_physical_device_(VkInstance instance, VkSurfaceKHR surface, + const std::vector &required_extensions); + + static util::Result> + enumerate_device_extensions_(VkPhysicalDevice physical_device); + + static bool + has_device_extension_props_(const std::vector &props, + const char *extension_name) noexcept; + + static util::Result> + build_device_extensions_(VkPhysicalDevice physical_device, + const std::vector &required); + + static util::Result + find_graphics_queue_family_or_error_(VkPhysicalDevice physical_device); + + static util::Result + find_present_queue_family_or_error_(VkPhysicalDevice physical_device, + VkSurfaceKHR surface); + VkPhysicalDevice physical_device_ = VK_NULL_HANDLE; VkDevice device_ = VK_NULL_HANDLE; VkQueue graphics_queue_ = VK_NULL_HANDLE; VkQueue present_queue_ = VK_NULL_HANDLE; uint32_t graphics_queue_family_index_ = 0; uint32_t present_queue_family_index_ = 0; + const VkAllocationCallbacks *alloc_ = nullptr; + DeviceCapabilities capabilities_{}; }; -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/include/quark/vk/device/details/device_capabilities.hpp b/include/quark/vk/device/details/device_capabilities.hpp new file mode 100644 index 0000000..b02ba56 --- /dev/null +++ b/include/quark/vk/device/details/device_capabilities.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +namespace quark::vk::details { + +// NOLINTNEXTLINE(performance-enum-size) +enum class DeviceFeature : uint64_t { + TimelineSemaphore = 1ULL << 0, + DynamicRendering = 1ULL << 1, + Synchronization2 = 1ULL << 2, +}; + +using DeviceFeatureFlags = uint64_t; + +/** + * @class DeviceCapabilities + * @brief Engine-visible summary of physical-device feature support and + * enablement. + * + * supported_features records what the queried physical device/runtime reports + * as available. enabled_features records what this engine actually requested + * and enabled when creating the logical device. + * + */ +struct DeviceCapabilities { + uint32_t api_version = VK_API_VERSION_1_0; + DeviceFeatureFlags supported_features = 0; + DeviceFeatureFlags enabled_features = 0; + + [[nodiscard]] bool api_at_least(uint32_t major, + uint32_t minor) const noexcept { + const auto vmaj = VK_VERSION_MAJOR(api_version); + const auto vmin = VK_VERSION_MINOR(api_version); + return (vmaj > major) || (vmaj == major && vmin >= minor); + } + + [[nodiscard]] bool supports(DeviceFeature feature) const noexcept { + return (supported_features & static_cast(feature)) != 0; + } + + [[nodiscard]] bool enabled(DeviceFeature feature) const noexcept { + return (enabled_features & static_cast(feature)) != 0; + } + + [[nodiscard]] bool is_vk12_or_newer() const noexcept { + return api_at_least(1, 2); + } + + [[nodiscard]] bool is_vk13_or_newer() const noexcept { + return api_at_least(1, 3); + } +}; + +} // namespace quark::vk::details diff --git a/include/quark/vk/device/details/device_feature_chain_builder.hpp b/include/quark/vk/device/details/device_feature_chain_builder.hpp new file mode 100644 index 0000000..cf244a5 --- /dev/null +++ b/include/quark/vk/device/details/device_feature_chain_builder.hpp @@ -0,0 +1,232 @@ +#pragma once + +#include +#include +#include + +namespace quark::vk::details { + +/** + * @enum WireFeatureStruct + * @brief Identifies which Vulkan feature struct own a feature field. + * + * DeviceFeatureChainBuilder stores features in a descriptor table and uses + * this to determine which Vulkan wire format struct should be read from or + * written to for a given feature. + */ +enum class WireFeatureStruct : uint8_t { + Vulkan12, + Vulkan13, +}; + +/** + * @class FeatureDescriptor + * @brief Static description of one engine-level device feature. + * + * A descriptor is the canonical registry entry for a feature within the + * builder. It ties together: + * - the engine-visible feature bit + * - a stable log/debug name + * - the Vulkan wire format field + */ +struct FeatureDescriptor { + /** + * @brief Engine-level feature bit represented by this descriptor. + */ + DeviceFeature feature{}; + + /** + * @brief Human-readable feature name used in logs/errors. + */ + const char *name = nullptr; + + /** + * @brief Identifies which Vulkan feature struct owns the mapped field. + */ + WireFeatureStruct wire_struct{}; + + VkBool32 VkPhysicalDeviceVulkan12Features::*vk12_member = nullptr; + VkBool32 VkPhysicalDeviceVulkan13Features::*vk13_member = nullptr; + + /** + * @brief Creates a descriptor for a Vulkan 1.2 feature field. + * + * @param feature Engine level feature bit. + * @param name Human-readable feature name. + * @param member Member pointer into VkPhysicalDeviceVulkan12Features. + * @return Fully initialized descriptor. + */ + [[nodiscard]] static constexpr FeatureDescriptor + make_vk12(DeviceFeature feature, const char *name, + VkBool32 VkPhysicalDeviceVulkan12Features::*member) noexcept { + return FeatureDescriptor{ + .feature = feature, + .name = name, + .wire_struct = WireFeatureStruct::Vulkan12, + .vk12_member = member, + .vk13_member = nullptr, + }; + } + + /** + * @brief Create a descriptor for a Vulkan 1.3 feature field. + * + * @param feature Engine level feature bit. + * @param name Human-readable feature name. + * @param member Member pointer into VkPhysicalDeviceVulkan13Features. + * @return Fully initialized descriptor. + */ + [[nodiscard]] static constexpr FeatureDescriptor + make_vk13(DeviceFeature feature, const char *name, + VkBool32 VkPhysicalDeviceVulkan13Features::*member) noexcept { + return FeatureDescriptor{ + .feature = feature, + .name = name, + .wire_struct = WireFeatureStruct::Vulkan13, + .vk12_member = nullptr, + .vk13_member = member, + }; + } +}; + +/** + * @class DeviceFeatureChainBuilder + * @brief Builds and owns the Vulkan feature query/create chain for logical + * device creation. + * + * This class centralizes Vulkan feature enabling and querying. This makes it + * simple for developers to add new necessary or optional features. + * + * The builder uses a descriptor table as the single registry of engine-visible + * features. This keeps feature support checks, enablement policy, and Vulkan + * field mapping consistent and reduces the number of places that must be edited + * when adding a new feature. + */ +class DeviceFeatureChainBuilder final { +public: + DeviceFeatureChainBuilder() = default; + + util::Status query_supported(VkPhysicalDevice physical_device, + DeviceCapabilities &capabilities); + + util::Status select_requested(DeviceFeatureFlags required, + DeviceFeatureFlags preferred, + DeviceCapabilities &capabilities); + + /** + * @brief Return the pNext chain root for VkDeviceCreateInfo. + * + * The returned pointer refers to storage owned by this builder and remains + * valid only while the builder object is alive. + * + * @return Pointer to the VkPhysicalDeviceFeatures2 root used for device + * creation. + */ + [[nodiscard]] const void *create_pnext() const noexcept { + return &features2_; + } + +private: + /** + * @brief Canonical registry of all device features known to this builder. + * + * Adding a new feature involves: + * - adding one descriptor entry + */ + static constexpr std::array kFeatureDescriptors{ + {FeatureDescriptor::make_vk12( + DeviceFeature::TimelineSemaphore, "timelineSemaphore", + &VkPhysicalDeviceVulkan12Features::timelineSemaphore), + FeatureDescriptor::make_vk13( + DeviceFeature::DynamicRendering, "dynamicRendering", + &VkPhysicalDeviceVulkan13Features::dynamicRendering), + FeatureDescriptor::make_vk13( + DeviceFeature::Synchronization2, "synchronization2", + &VkPhysicalDeviceVulkan13Features::synchronization2)}}; + + /** + * @brief Test whether a feature bit is present in a bitmask. + * + * @param flags flags Bitmask to test. + * @param feature Feature bit to check. + * @return True is the feature bit is set. + */ + [[nodiscard]] static bool has_flag_(DeviceFeatureFlags flags, + DeviceFeature feature) noexcept { + return (flags & static_cast(feature)) != 0; + } + + /** + * @brief Set or clear a feature bit in a bitmask. + * + * @param flags Bitmask to update. + * @param feature Feature bit to modify. + * @param value True to set the bit, false to clear it. + */ + static void set_flag_(DeviceFeatureFlags &flags, DeviceFeature feature, + bool value) noexcept; + + /** + * @brief Read one supported feature value from the Vulkan wire chain. + * + * @param desc Descriptor identifying which semantic bool to access. + * @return True if the feature is reported supported by the quired wire chain. + */ + [[nodiscard]] bool + read_supported_feature_(const FeatureDescriptor &desc) const noexcept; + + /** + * @brief Write one requested feature value into the Vulkan create chain. + * + * @param desc Descriptor identifying which semantic bool to access. + * @param value Requested semantic value to serialize into the chain. + */ + void write_requested_feature_(const FeatureDescriptor &desc, + bool value) noexcept; + + /** + * @brief Initialize the Vulkan feature chain structure. + * + * Prepares the VkPhysicalDeviceFeatures2 root and attaches the versioned + * Vulkan feature structs int he correct pNext order. + */ + void reset_chain_() noexcept; + + /** + * @brief Apply required/preferred feature policy to one feature. + * + * @param feature Engine-level feature bit. + * @param name Human-readable feature name for logs/errors. + * @param supported Whether the feature is supported by the queried device. + * @param required Whether the feature is required by the engine policy. + * @param preferred Whether the feature is preferred by the engine policy. + * @param requested_out Ouput set to true when the feature should be enabled. + * @param capabilities Capability record updated with enabled feature bits. + * @return Success of an unsupported-feature error when a required feature is + * unavailable. + */ + util::Status static maybe_enable_(DeviceFeature feature, const char *name, + bool supported, bool required, + bool preferred, bool &requested_out, + DeviceCapabilities &capabilities); + + /** + * @brief Bitset of features reported supported by the physical device. + */ + DeviceFeatureFlags supported_features_ = 0; + + /** + * @brief Bitset of features selected for logical-device creation. + */ + DeviceFeatureFlags requested_features_ = 0; + + /** + * @brief Root of the Vulkan feature query/create chain. + */ + VkPhysicalDeviceFeatures2 features2_{}; + + VkPhysicalDeviceVulkan12Features vk12_{}; + VkPhysicalDeviceVulkan13Features vk13_{}; +}; + +} // namespace quark::vk::details diff --git a/include/quark/vk/device/details/device_handle.hpp b/include/quark/vk/device/details/device_handle.hpp index dedf8f9..1a242bf 100644 --- a/include/quark/vk/device/details/device_handle.hpp +++ b/include/quark/vk/device/details/device_handle.hpp @@ -2,7 +2,7 @@ #include -namespace quark::vk { +namespace quark::vk::details { struct DeviceHandle { uint32_t index = 0xFFFF'FFFFU; @@ -18,4 +18,4 @@ struct DeviceHandle { } }; -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/include/quark/vk/device/details/device_registry.hpp b/include/quark/vk/device/details/device_registry.hpp index 227c0fd..7f70c57 100644 --- a/include/quark/vk/device/details/device_registry.hpp +++ b/include/quark/vk/device/details/device_registry.hpp @@ -8,7 +8,7 @@ #include #include -namespace quark::vk { +namespace quark::vk::details { class DeviceRegistry final { public: @@ -41,4 +41,4 @@ class DeviceRegistry final { std::vector free_; }; -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/include/quark/vk/device/device_bundle.hpp b/include/quark/vk/device/device_bundle.hpp index 2a6908b..af72db4 100644 --- a/include/quark/vk/device/device_bundle.hpp +++ b/include/quark/vk/device/device_bundle.hpp @@ -1,27 +1,41 @@ #pragma once -#include "quark/utils/result.hpp" #include -#include -#include +#include #include namespace quark::vk { +namespace details { + +class Device; +struct DeviceHandle; + +} // namespace details + +struct DeviceView; + class DeviceBundle final { public: + struct CreateInfo { + details::Device::CreateInfo device{}; + }; + DeviceBundle() = default; - explicit DeviceBundle(const Device::CreateInfo &ci); + explicit DeviceBundle(const DeviceBundle::CreateInfo &ci); ~DeviceBundle() { destroy(); } QUARK_MOVE_ONLY(DeviceBundle); - util::Status create(const Device::CreateInfo &ci); + util::Status create(const DeviceBundle::CreateInfo &ci); void destroy() noexcept; - [[nodiscard]] bool valid() const noexcept { return registry_.alive(handle_); } + [[nodiscard]] util::Status validate() const noexcept; - [[nodiscard]] DeviceHandle handle() const noexcept { return handle_; } + [[nodiscard]] details::DeviceHandle handle() const noexcept { + return handle_; + } + [[nodiscard]] DeviceView view() const noexcept; [[nodiscard]] VkPhysicalDevice vk_physical_device() const noexcept; [[nodiscard]] VkDevice vk_device() const noexcept; @@ -29,10 +43,11 @@ class DeviceBundle final { [[nodiscard]] VkQueue present_queue() const noexcept; [[nodiscard]] uint32_t graphics_queue_family_index() const noexcept; [[nodiscard]] uint32_t present_queue_family_index() const noexcept; + [[nodiscard]] details::DeviceCapabilities capabilities() const noexcept; private: - DeviceRegistry registry_; - DeviceHandle handle_{}; + details::DeviceRegistry registry_; + details::DeviceHandle handle_{}; }; } // namespace quark::vk diff --git a/include/quark/vk/device/device_view.hpp b/include/quark/vk/device/device_view.hpp new file mode 100644 index 0000000..7f99604 --- /dev/null +++ b/include/quark/vk/device/device_view.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +namespace quark::vk { + +struct DeviceView { + VkPhysicalDevice physical_device = VK_NULL_HANDLE; + VkDevice device = VK_NULL_HANDLE; + + VkQueue graphics_queue = VK_NULL_HANDLE; + uint32_t graphics_queue_family_index = 0; +}; + +[[nodiscard]] util::Status validate(const DeviceView &view) noexcept; + +} // namespace quark::vk diff --git a/include/quark/vk/diagnostic_prelude.hpp b/include/quark/vk/diagnostic_prelude.hpp index 60e4491..1f2c031 100644 --- a/include/quark/vk/diagnostic_prelude.hpp +++ b/include/quark/vk/diagnostic_prelude.hpp @@ -4,6 +4,22 @@ #include #include +/** + * @def QUARK_VK_TRY(call) + * @brief Executes a Vulkan call and converts VkResult failure into util::Error. + * + * Evaluates @p call exactly once. If it returns VK_SUCCESS, continues. + * Otherwise returns util::unexpected(vk_error(result, "call")) from the current + * function. + * + * Requirements: + * - Current function returns util::Status or compatible util::Result. + * - vk_error(VkResult, const char*) must construct a util::Error with an API + * domain payload, message, and call-site location. + * + * Notes: + * - The stringified expression (#call) is captured for diagnostics. + */ #define QUARK_VK_TRY(call) \ do { \ const VkResult _q_vk_r = (call); \ @@ -12,6 +28,13 @@ } \ } while (0) +/** + * @def QUARK_VK_TRY_INCOMPLETE_OK(call) + * @brief Like QUARK_VK_TRY, but treats VK_INCOMPLETE as success. + * + * Use for Vulkan enumeration patterns where VK_INCOMPLETE indicates that the + * provided buffer was too small and the caller will retry. + */ #define QUARK_VK_TRY_INCOMPLETE_OK(call) \ do { \ const VkResult _q_vk_r = (call); \ diff --git a/include/quark/vk/frame/details/frame_cmd.hpp b/include/quark/vk/frame/details/frame_cmd.hpp new file mode 100644 index 0000000..9728f82 --- /dev/null +++ b/include/quark/vk/frame/details/frame_cmd.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace quark::vk::details { + +class FrameCmd final { +public: + struct CreateInfo { + DeviceView device; + uint32_t buffer_count = 0; + VkCommandPoolCreateFlags pool_flags = + VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + const VkAllocationCallbacks *allocator = nullptr; + }; + + FrameCmd() = default; + ~FrameCmd() { destroy(); } + + QUARK_MOVE_ONLY(FrameCmd); + + util::Status create(const CreateInfo &ci); + void destroy() noexcept; + + [[nodiscard]] bool valid() const noexcept { + return vk_device_ != VK_NULL_HANDLE && pool_ != VK_NULL_HANDLE; + } + + [[nodiscard]] VkCommandPool pool() const noexcept { return pool_; } + + // One primary per swapchain image + [[nodiscard]] VkCommandBuffer cmd(uint32_t index) const noexcept { + return (index < buffers_.size()) ? buffers_[index] : VK_NULL_HANDLE; + } + + [[nodiscard]] uint32_t count() const noexcept { + return static_cast(buffers_.size()); + } + + // Clear and reallocate buffers for new swapchain + util::Status resize(uint32_t new_count); + +private: + void free_buffers_() noexcept; + + VkDevice vk_device_ = VK_NULL_HANDLE; // non-owning + uint32_t graphics_qfi_ = 0; // non-owning + const VkAllocationCallbacks *alloc_ = nullptr; + + VkCommandPool pool_ = VK_NULL_HANDLE; + std::vector buffers_; +}; + +} // namespace quark::vk::details diff --git a/include/quark/vk/frame/details/frame_handle.hpp b/include/quark/vk/frame/details/frame_handle.hpp new file mode 100644 index 0000000..2b17fc4 --- /dev/null +++ b/include/quark/vk/frame/details/frame_handle.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +namespace quark::vk::details { + +struct FrameHandle { + uint32_t index = 0; + uint32_t generation = 0; + + [[nodiscard]] bool valid() const noexcept { return generation != 0; } +}; + +} // namespace quark::vk::details diff --git a/include/quark/vk/frame/details/frame_registry.hpp b/include/quark/vk/frame/details/frame_registry.hpp new file mode 100644 index 0000000..8d6f23f --- /dev/null +++ b/include/quark/vk/frame/details/frame_registry.hpp @@ -0,0 +1,81 @@ +#pragma once + +// Can we make forward declare friendly? +#include "quark/engine/retire/retirement_queue.hpp" +#include "quark/vk/device/device_view.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace quark::vk::details { + +struct FrameHandle; + +class FrameRegistry final { +public: + struct CreateInfo { + DeviceView device{}; + RetirementQueue *retire_queue = nullptr; + + uint32_t frames_in_flight = 0; + uint32_t cmd_buffer_count = 0; + + VkCommandPoolCreateFlags cmd_pool_flags = + VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + + const VkAllocationCallbacks *allocator = nullptr; + bool frames_idle_on_create = true; + }; + + FrameRegistry() = default; + ~FrameRegistry() { clear(); } + + QUARK_MOVE_ONLY(FrameRegistry); + + [[nodiscard]] util::Result create(const CreateInfo &ci); + void destory(FrameHandle handle, uint64_t retire_at) noexcept; + + // TODO: add validate() + + void clear() noexcept; + + [[nodiscard]] bool alive(FrameHandle handle) const noexcept; + + [[nodiscard]] FrameCmd *cmd(FrameHandle handle) noexcept; + [[nodiscard]] const FrameCmd *cmd(FrameHandle handle) const noexcept; + + [[nodiscard]] FrameSync *sync(FrameHandle handle) noexcept; + [[nodiscard]] const FrameSync *sync(FrameHandle handle) const noexcept; + +private: + struct Slot { + bool live = false; + uint32_t generation = 1; + + FrameCmd cmd; + FrameSync sync; + }; + + struct RetiredFramePayload { + FrameCmd cmd; + FrameSync sync; + }; + + static void destroy_retired_frame_(void *ctx) noexcept; + static void cleanup_retired_frame_(void *ctx) noexcept; + + [[nodiscard]] static bool matches_(FrameHandle handle, + const Slot &slot) noexcept; + + RetirementQueue *retire_queue_ = nullptr; + + std::vector slots_; + std::vector free_; +}; + +} // namespace quark::vk::details diff --git a/include/quark/vk/frame/details/frame_sync.hpp b/include/quark/vk/frame/details/frame_sync.hpp new file mode 100644 index 0000000..3fe046c --- /dev/null +++ b/include/quark/vk/frame/details/frame_sync.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace quark::vk::details { + +class FrameSync final { +public: + struct CreateInfo { + DeviceView device{}; + uint32_t frames_in_flight = 0; + const VkAllocationCallbacks *allocator = nullptr; + + // If true, frames start idle + // If false, one can pre-seed values + bool frames_idle_on_create = true; + }; + + FrameSync() = default; + ~FrameSync() { destroy(); } + + QUARK_MOVE_ONLY(FrameSync); + + util::Status create(const CreateInfo &ci); + void destroy() noexcept; + + util::Status resize(uint32_t new_frames_in_flight); + + [[nodiscard]] bool valid() const noexcept { return !frames_.empty(); } + + [[nodiscard]] uint32_t frame_count() const noexcept { + return static_cast(frames_.size()); + } + + [[nodiscard]] VkSemaphore image_available(uint32_t frame) const noexcept { + return (frame < frames_.size()) ? frames_[frame].image_available + : VK_NULL_HANDLE; + } + + [[nodiscard]] VkSemaphore render_finished(uint32_t frame) const noexcept { + return (frame < frames_.size()) ? frames_[frame].render_finished + : VK_NULL_HANDLE; + } + + /** + * Last submitted timeline value for this frame slot. + * + * 0 -> nothing submitted yet. + */ + [[nodiscard]] uint64_t in_flight_value(uint32_t frame) const noexcept { + return (frame < frames_.size()) ? frames_[frame].in_flight_value : 0; + } + + /// After vkQueueSubmit, record the value that represents frame slot is in + /// flight. + void mark_submitted(uint32_t frame, uint64_t value) noexcept { + if (frame < frames_.size()) { + frames_[frame].in_flight_value = value; + } + } + +private: + struct PerFrame { + VkSemaphore image_available = VK_NULL_HANDLE; + VkSemaphore render_finished = VK_NULL_HANDLE; + uint64_t in_flight_value = 0; + }; + + util::Status create_per_frame_(uint32_t count); + void destroy_per_frame_() noexcept; + + VkDevice vk_device_ = VK_NULL_HANDLE; // non-owning + const VkAllocationCallbacks *alloc_ = nullptr; + + bool frames_idle_on_create_ = true; + std::vector frames_; +}; + +} // namespace quark::vk::details diff --git a/include/quark/vk/frame/frame_bundle.hpp b/include/quark/vk/frame/frame_bundle.hpp new file mode 100644 index 0000000..e1d9d64 --- /dev/null +++ b/include/quark/vk/frame/frame_bundle.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace quark::vk { + +namespace details { + +class FrameSync; +class FrameCmd; + +} // namespace details + +class FrameBundle final { +public: + using CreateInfo = details::FrameRegistry::CreateInfo; + + FrameBundle() = default; + ~FrameBundle() { destroy(); } + + QUARK_MOVE_ONLY(FrameBundle); + + [[nodiscard]] util::Status create(const CreateInfo &ci); + void destroy() noexcept; + + [[nodiscard]] bool valid() const noexcept { return registry_.alive(handle_); } + + [[nodiscard]] FrameView view() noexcept; + [[nodiscard]] ConstFrameView view() const noexcept; + + [[nodiscard]] details::FrameCmd *cmd() noexcept; + [[nodiscard]] const details::FrameCmd *cmd() const noexcept; + [[nodiscard]] details::FrameSync *sync() noexcept; + [[nodiscard]] const details::FrameSync *sync() const noexcept; + + /** + * @brief Retires the current frame set after the given timeline value. + * + * After this call, the bundle becomes invalid immediately on the CPU side. + */ + void retire(uint64_t retire_at) noexcept; + +private: + details::FrameRegistry registry_; + details::FrameHandle handle_{}; +}; + +} // namespace quark::vk diff --git a/include/quark/vk/frame/frame_view.hpp b/include/quark/vk/frame/frame_view.hpp new file mode 100644 index 0000000..cf5e79c --- /dev/null +++ b/include/quark/vk/frame/frame_view.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +namespace quark::vk { + +namespace details { + +class FrameCmd; +class FrameSync; + +} // namespace details + +struct FrameView { + details::FrameHandle handle{}; + + details::FrameCmd *cmd = nullptr; + details::FrameSync *sync = nullptr; + + [[nodiscard]] bool valid() const noexcept { + return handle.valid() && cmd != nullptr && sync != nullptr; + } +}; + +struct ConstFrameView { + details::FrameHandle handle{}; + + const details::FrameCmd *cmd = nullptr; + const details::FrameSync *sync = nullptr; + + [[nodiscard]] bool valid() const noexcept { + return handle.valid() && cmd != nullptr && sync != nullptr; + } +}; + +} // namespace quark::vk diff --git a/include/quark/vk/instance/details/debug_messenger.hpp b/include/quark/vk/instance/details/debug_messenger.hpp index b18196a..d34a68e 100644 --- a/include/quark/vk/instance/details/debug_messenger.hpp +++ b/include/quark/vk/instance/details/debug_messenger.hpp @@ -3,9 +3,9 @@ #include #include #include -#include +#include -namespace quark::vk { +namespace quark::vk::details { class DebugMessenger final { public: @@ -21,6 +21,7 @@ class DebugMessenger final { PFN_vkDebugUtilsMessengerCallbackEXT callback = nullptr; void *user_data = nullptr; + const VkAllocationCallbacks *allocator = nullptr; }; DebugMessenger() = default; @@ -42,6 +43,7 @@ class DebugMessenger final { VkInstance instance_ = VK_NULL_HANDLE; // non-owning VkDebugUtilsMessengerEXT messenger_ = VK_NULL_HANDLE; + const VkAllocationCallbacks *alloc_ = nullptr; }; -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/include/quark/vk/instance/details/instance.hpp b/include/quark/vk/instance/details/instance.hpp index bbddf1b..efa1b2a 100644 --- a/include/quark/vk/instance/details/instance.hpp +++ b/include/quark/vk/instance/details/instance.hpp @@ -8,7 +8,7 @@ #include #include -namespace quark::vk { +namespace quark::vk::details { class Instance final { public: @@ -24,6 +24,8 @@ class Instance final { // Validation layers std::vector layers; + const VkAllocationCallbacks *allocator = nullptr; + bool enable_debug_messenger = false; DebugMessenger::CreateInfo debug{}; }; @@ -65,7 +67,8 @@ class Instance final { VkInstanceCreateFlags &out_flags, bool &out_enable_debug_utils); VkInstance instance_ = VK_NULL_HANDLE; + const VkAllocationCallbacks *alloc_ = nullptr; DebugMessenger debug_messenger_; }; -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/include/quark/vk/instance/details/instance_handle.hpp b/include/quark/vk/instance/details/instance_handle.hpp index 0b7b930..3695e36 100644 --- a/include/quark/vk/instance/details/instance_handle.hpp +++ b/include/quark/vk/instance/details/instance_handle.hpp @@ -2,7 +2,7 @@ #include -namespace quark::vk { +namespace quark::vk::details { /** * @brief Opaque handle to a registry-managed InstanceBundle. @@ -23,4 +23,4 @@ struct InstanceHandle { } }; -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/include/quark/vk/instance/details/instance_registry.hpp b/include/quark/vk/instance/details/instance_registry.hpp index c103724..b3b97aa 100644 --- a/include/quark/vk/instance/details/instance_registry.hpp +++ b/include/quark/vk/instance/details/instance_registry.hpp @@ -8,7 +8,7 @@ #include #include -namespace quark::vk { +namespace quark::vk::details { class InstanceRegistry final { public: @@ -42,4 +42,4 @@ class InstanceRegistry final { std::vector free_; }; -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/include/quark/vk/instance/instance_bundle.hpp b/include/quark/vk/instance/instance_bundle.hpp index bd63359..2a565d8 100644 --- a/include/quark/vk/instance/instance_bundle.hpp +++ b/include/quark/vk/instance/instance_bundle.hpp @@ -2,38 +2,73 @@ #include #include -#include -#include -#include #include #include +#include namespace quark::vk { +namespace details { + +class DebugMessenger; +class Instance; +struct InstanceHandle; + +} // namespace details + +/** + * @brief Owns a Vulkan instance (VkInstance). + * + * This is the public facade used by engine code outside the instance subsystem. + * Internally it stores an opaque handle into details::InstanceRegistry. + * + * Lifetime: + * - create() acquires a live instance slot in the registry + * - destroy() release it (idempotent). + * + * Threading: + * - Not thread-safe; external synchronization required. + */ class InstanceBundle final { public: + struct CreateInfo { + /// User facing instance settings. + details::Instance::CreateInfo instance{}; + }; + InstanceBundle() = default; ~InstanceBundle() { destroy(); } // TODO: assert safe move semantics in instance and debug messenger QUARK_MOVE_ONLY(InstanceBundle); - [[nodiscard]] util::Status create(const Instance::CreateInfo &ci); + /// Acquires a live instance slot in the registry + [[nodiscard]] util::Status create(const CreateInfo &ci); + + /// Releases the live instance slot in the registry (idempotent). void destroy() noexcept; [[nodiscard]] bool valid() const noexcept { return registry_.alive(handle_); } - /// Opaque handle - [[nodiscard]] InstanceHandle handle() const noexcept { return handle_; } + /** + * Opaque handle for diagnostics/registries; avoid using in higher layers. + */ + [[nodiscard]] details::InstanceHandle handle() const noexcept { + return handle_; + } - /// Resolve to raw VkInstance. + /** + * Resolves to raw VkInstance. + * + * Equivalent to view().instance. + */ [[nodiscard]] VkInstance vk_instance() const noexcept; - [[nodiscard]] const DebugMessenger *debug_messenger() const noexcept; + [[nodiscard]] const details::DebugMessenger *debug_messenger() const noexcept; private: - InstanceRegistry registry_; - InstanceHandle handle_{}; + details::InstanceRegistry registry_; + details::InstanceHandle handle_{}; }; } // namespace quark::vk diff --git a/include/quark/vk/presentation/details/swapchain.hpp b/include/quark/vk/presentation/details/swapchain.hpp index 8427475..e83ff97 100644 --- a/include/quark/vk/presentation/details/swapchain.hpp +++ b/include/quark/vk/presentation/details/swapchain.hpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include @@ -30,9 +30,12 @@ class Swapchain final { Swapchain() = default; ~Swapchain() { reset(); } - QUARK_MOVE_ONLY(Swapchain); + Swapchain(const Swapchain &) = delete; + Swapchain &operator=(const Swapchain &) = delete; + Swapchain(Swapchain &&other) noexcept; + Swapchain &operator=(Swapchain &&other) noexcept; - void create(const CreateInfo &ci); + [[nodiscard]] util::Status create(const CreateInfo &ci); void reset() noexcept; [[nodiscard]] VkSwapchainKHR handle() const noexcept { return swapchain_; } diff --git a/include/quark/vk/presentation/presenter_bundle.hpp b/include/quark/vk/presentation/presenter_bundle.hpp new file mode 100644 index 0000000..2ad1dc8 --- /dev/null +++ b/include/quark/vk/presentation/presenter_bundle.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +namespace quark::vk { + +class PresenterBundle final { +public: + struct CreateInfo { + VkInstance instance = VK_NULL_HANDLE; + VkPhysicalDevice physical_device = VK_NULL_HANDLE; + VkDevice device = VK_NULL_HANDLE; + + uint32_t graphics_queue_family_index = 0; + uint32_t present_queue_family_index = 0; + + const platform::IWindow *window = nullptr; + + VkPresentModeKHR preferred_present_mode = VK_PRESENT_MODE_MAILBOX_KHR; + VkFormat preferred_format = VK_FORMAT_B8G8R8A8_SRGB; + VkColorSpaceKHR preferred_color_space = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + }; + + PresenterBundle() = default; + ~PresenterBundle() { destroy(); } + + QUARK_MOVE_ONLY(PresenterBundle); + + [[nodiscard]] util::Status create(const CreateInfo &ci); + [[nodiscard]] util::Status recreate_swapchain(); + void destroy_swapchain() noexcept; + void destroy() noexcept; + + [[nodiscard]] bool valid() const noexcept { + return surface_ != VK_NULL_HANDLE && swapchain_.handle() != VK_NULL_HANDLE; + } + + [[nodiscard]] VkSurfaceKHR surface() const noexcept { return surface_; } + [[nodiscard]] const Swapchain &swapchain() const noexcept { + return swapchain_; + } + [[nodiscard]] Swapchain &swapchain() noexcept { return swapchain_; } + +private: + CreateInfo create_info_{}; + VkSurfaceKHR surface_{VK_NULL_HANDLE}; + Swapchain swapchain_; +}; + +} // namespace quark::vk \ No newline at end of file diff --git a/include/quark/vk/sync/gpu_timeline.hpp b/include/quark/vk/sync/gpu_timeline.hpp new file mode 100644 index 0000000..4e3ec8f --- /dev/null +++ b/include/quark/vk/sync/gpu_timeline.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace quark::vk { + +class GpuTimeline final { +public: + struct CreateInfo { + DeviceView device{}; + const VkAllocationCallbacks *allocator = nullptr; + + /// Initial semaphore counter value. + uint64_t initial_value = 0; + + /// First value returned by next_signal_value(). + uint64_t first_signal_value = 0; + }; + + GpuTimeline() = default; + ~GpuTimeline() { destroy(); } + + QUARK_MOVE_ONLY(GpuTimeline); + + util::Status create(const CreateInfo &ci); + void destroy() noexcept; + + // TODO: make into a validate + [[nodiscard]] bool valid() const noexcept { + return vk_device_ != VK_NULL_HANDLE && timeline_ != VK_NULL_HANDLE; + } + + [[nodiscard]] VkSemaphore semaphore() const noexcept { return timeline_; } + + /** + * @brief Returns a unique value to be signaled on the next submission. + * + * Contract: + * - Values are monotonically increasing. + * - Caller must ensure submissions signal the returned value on this + * timeline. + */ + [[nodiscard]] uint64_t next_signal_value() noexcept { return ++next_value_; } + + /** + * @brief Returns the current completed counter value of the timeline. + */ + [[nodiscard]] util::Result completed_value() const noexcept; + + // Wait until completed >= value. + // + + /** + * @brief Wait until the timeline has reached at least @p value. + * + * No-op if @p value == 0. + * + * @param value The target counter value. + * @param timeout_ns Timeout in nanoseconds; UINT64_MAX waits indefinitely. + */ + [[nodiscard]] util::Status wait(uint64_t value, + uint64_t timeout_ns = UINT64_MAX) const; + +private: + VkDevice vk_device_ = VK_NULL_HANDLE; // non-owning + const VkAllocationCallbacks *alloc_ = nullptr; + + VkSemaphore timeline_ = VK_NULL_HANDLE; + + uint64_t next_value_ = 1; +}; + +} // namespace quark::vk diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 466fa4c..6432171 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,59 +1,62 @@ add_library(quark_build_settings INTERFACE) add_library(quark::build_settings ALIAS quark_build_settings) -target_include_directories(quark_build_settings INTERFACE - $ - $ -) +target_include_directories( + quark_build_settings + INTERFACE $ + $) target_compile_features(quark_build_settings INTERFACE cxx_std_23) -target_compile_options(quark_build_settings INTERFACE - $<$:/W4 /permissive- /Zc:preprocessor> - $<$>:-Wall -Wextra -Wpedantic -Wshadow -pedantic-errors> -) +target_compile_options( + quark_build_settings + INTERFACE $<$:/W4 + /permissive- + /Zc:preprocessor> + $<$>:-Wall + -Wextra + -Wpedantic + -Wshadow + -pedantic-errors>) add_library(quark_headers_core INTERFACE) add_library(quark::headers::core ALIAS quark_headers_core) -target_link_libraries(quark_headers_core - INTERFACE - quark::build_settings - fmt::fmt -) +target_link_libraries(quark_headers_core INTERFACE quark::build_settings + fmt::fmt) add_library(quark_headers_vk INTERFACE) add_library(quark::headers::vk ALIAS quark_headers_vk) -target_link_libraries(quark_headers_vk - INTERFACE - quark::headers::core - Vulkan::Vulkan -) +target_link_libraries(quark_headers_vk INTERFACE quark::headers::core + Vulkan::Vulkan) add_subdirectory(backend) +add_subdirectory(engine) add_subdirectory(platform) add_subdirectory(utils) add_executable(quark_app main.cpp) -set_target_properties(quark_app PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" -) - -target_link_libraries(quark_app PRIVATE - quark::backend - quark::platform - quark::utils -) - -target_compile_options(quark_app PRIVATE - $<$:/W4 /permissive /Zc:preprocessor-> - $<$>:-Wall -Wextra -Wpedantic -Wshadow -pedantic-errors> -) - -add_custom_target(run +set_target_properties(quark_app PROPERTIES RUNTIME_OUTPUT_DIRECTORY + "${CMAKE_BINARY_DIR}/bin") + +target_link_libraries(quark_app PRIVATE quark::backend quark::engine + quark::platform quark::utils) + +target_compile_options( + quark_app + PRIVATE $<$:/W4 + /permissive + /Zc:preprocessor-> + $<$>:-Wall + -Wextra + -Wpedantic + -Wshadow + -pedantic-errors>) + +add_custom_target( + run COMMAND $ DEPENDS quark_app WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" - USES_TERMINAL -) + USES_TERMINAL) quark_apply_sanitizers(quark_app) quark_apply_lto(quark_app) diff --git a/src/backend/vulkan/CMakeLists.txt b/src/backend/vulkan/CMakeLists.txt index 1627912..ac6b4f0 100644 --- a/src/backend/vulkan/CMakeLists.txt +++ b/src/backend/vulkan/CMakeLists.txt @@ -1,20 +1,18 @@ -add_library(quark_backend_vulkan STATIC - vulkan_context.cpp -) +add_library(quark_backend_vulkan STATIC vulkan_context.cpp) add_library(quark::backend::vulkan ALIAS quark_backend_vulkan) add_subdirectory(instance) add_subdirectory(device) +add_subdirectory(frame) +add_subdirectory(presentation) +add_subdirectory(sync) -target_link_libraries(quark_backend_vulkan - INTERFACE - quark::backend::vulkan::instance - quark::backend::vulkan::device - PUBLIC - quark::headers::vk -) +target_link_libraries( + quark_backend_vulkan + PRIVATE quark::backend::vulkan::instance quark::backend::vulkan::device + quark::backend::vulkan::frame quark::backend::vulkan::presentation + quark::backend::vulkan::sync + PUBLIC quark::headers::vk) target_include_directories(quark_backend_vulkan - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src -) + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) diff --git a/src/backend/vulkan/device/CMakeLists.txt b/src/backend/vulkan/device/CMakeLists.txt index 9bf2328..9bbebbf 100644 --- a/src/backend/vulkan/device/CMakeLists.txt +++ b/src/backend/vulkan/device/CMakeLists.txt @@ -1,16 +1,10 @@ -add_library(quark_backend_vulkan_device STATIC - device.cpp - device_registry.cpp - device_bundle.cpp -) +add_library( + quark_backend_vulkan_device STATIC + device.cpp device_feature_chain_builder.cpp device_registry.cpp + device_bundle.cpp device_view.cpp) add_library(quark::backend::vulkan::device ALIAS quark_backend_vulkan_device) -target_link_libraries(quark_backend_vulkan_device - PUBLIC - quark::headers::vk -) +target_link_libraries(quark_backend_vulkan_device PUBLIC quark::headers::vk) target_include_directories(quark_backend_vulkan_device - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} -) + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/backend/vulkan/device/device.cpp b/src/backend/vulkan/device/device.cpp index 28f63af..392ebe8 100644 --- a/src/backend/vulkan/device/device.cpp +++ b/src/backend/vulkan/device/device.cpp @@ -1,153 +1,108 @@ #include #include #include -#include #include +#include #include #include #include #include #include -using std::vector; +namespace quark::vk::details { -namespace quark::vk { - -namespace { - -struct DeviceSelection { - VkPhysicalDevice physical_device{VK_NULL_HANDLE}; - uint32_t graphics_queue_family_index{0}; - uint32_t present_queue_family_index{0}; -}; - -struct SwapchainSupportDetails { - VkSurfaceCapabilitiesKHR capabilities{}; - vector formats; - vector present_modes; -}; - -[[nodiscard]] util::Result> -enumerate_device_extensions(VkPhysicalDevice physical_device) { - QUARK_ENSURE(physical_device != VK_NULL_HANDLE, - QUARK_ERR(util::Errc::InvalidArg, "physical_device is null")); +util::Status Device::create(const Device::CreateInfo &ci) { + destroy(); - while (true) { - uint32_t count = 0; - QUARK_VK_TRY(vkEnumerateDeviceExtensionProperties( - physical_device, /*pLayerName=*/nullptr, &count, - /*pProperties=*/nullptr)); + QUARK_ENSURE(ci.instance != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidArg, + "Cannot create Vulkan device with null instance")); + QUARK_ENSURE(ci.surface != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidArg, + "Cannot create Vulkan device with null surface")); - vector props(count); + alloc_ = ci.allocator; - uint32_t written = count; - const VkResult r = vkEnumerateDeviceExtensionProperties( - physical_device, nullptr, &written, props.data()); + DeviceSelection selection{}; + QUARK_TRY_ASSIGN(selection, pick_physical_device_(ci.instance, ci.surface, + ci.required_extensions)); - if (r == VK_SUCCESS) { - props.resize(written); - return props; - } + physical_device_ = selection.physical_device; + graphics_queue_family_index_ = selection.graphics_queue_family_index; + present_queue_family_index_ = selection.present_queue_family_index; - if (r != VK_INCOMPLETE) { - return util::unexpected( - vk_error(r, "vkEnumerateDeviceExtensionProperties(data)", - std::source_location::current())); - } - } -} + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(physical_device_, &properties); + capabilities_.api_version = properties.apiVersion; + capabilities_.supported_features = 0; + capabilities_.enabled_features = 0; -[[nodiscard]] bool -has_device_extension_props(const vector &props, - const char *extension_name) noexcept { - if (extension_name == nullptr || extension_name[0] == '\0') { - return false; - } + constexpr float queue_priority{1.0F}; + const std::set unique_queue_families{graphics_queue_family_index_, + present_queue_family_index_}; - for (const auto &p : props) { - if (std::strcmp(p.extensionName, extension_name) == 0) { - return true; - } + std::vector queue_create_infos; + queue_create_infos.reserve(unique_queue_families.size()); + for (const uint32_t queue_family : unique_queue_families) { + VkDeviceQueueCreateInfo queue_create_info{}; + queue_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queue_create_info.queueFamilyIndex = queue_family; + queue_create_info.queueCount = 1; + queue_create_info.pQueuePriorities = &queue_priority; + queue_create_infos.push_back(queue_create_info); } - return false; -} - -[[nodiscard]] util::Result -has_required_extensions(VkPhysicalDevice physical_device, - const vector &required_extensions) { - vector props; - QUARK_TRY_ASSIGN(props, enumerate_device_extensions(physical_device)); - - for (const char *name : required_extensions) { - QUARK_ENSURE(name != nullptr, QUARK_ERR(util::Errc::InvalidArg, - "required extension name is null")); - QUARK_ENSURE( - name[0] != '\0', - QUARK_ERR(util::Errc::InvalidArg, "required extension name is empty")); - - if (!has_device_extension_props(props, name)) { - return false; - } - } + std::vector enabled_device_extensions; + QUARK_TRY_ASSIGN( + enabled_device_extensions, + build_device_extensions_(physical_device_, ci.required_extensions)); - return true; -} + DeviceFeatureChainBuilder feature_builder; + QUARK_TRY_STATUS( + feature_builder.query_supported(physical_device_, capabilities_)); + QUARK_TRY_STATUS(feature_builder.select_requested( + ci.required_features, ci.preferred_features, capabilities_)); -[[nodiscard]] std::optional -find_graphics_queue_family(VkPhysicalDevice physical_device) { - uint32_t queue_family_count{0}; - vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, - nullptr); + VkDeviceCreateInfo create_info{}; + create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + create_info.pNext = feature_builder.create_pnext(); + create_info.pQueueCreateInfos = queue_create_infos.data(); + create_info.queueCreateInfoCount = + static_cast(queue_create_infos.size()); + create_info.enabledExtensionCount = + static_cast(enabled_device_extensions.size()); + create_info.ppEnabledExtensionNames = enabled_device_extensions.empty() + ? nullptr + : enabled_device_extensions.data(); + create_info.pEnabledFeatures = nullptr; - vector queue_families(queue_family_count); - vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, - queue_families.data()); + VkDevice out = VK_NULL_HANDLE; + QUARK_VK_TRY(vkCreateDevice(physical_device_, &create_info, alloc_, &out)); + device_ = out; - for (auto index{0UZ}; index < queue_family_count; ++index) { - if ((queue_families[index].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0U) { - return index; - } - } + vkGetDeviceQueue(device_, graphics_queue_family_index_, 0, &graphics_queue_); + vkGetDeviceQueue(device_, present_queue_family_index_, 0, &present_queue_); - return std::nullopt; + QUARK_OK(); } -[[nodiscard]] SwapchainSupportDetails -query_swapchain_support(VkPhysicalDevice physical_device, - VkSurfaceKHR surface) { - SwapchainSupportDetails details; - - vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, surface, - &details.capabilities); - - uint32_t format_count{0}; - vkGetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface, &format_count, - nullptr); - if (format_count > 0) { - details.formats.resize(format_count); - vkGetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface, - &format_count, details.formats.data()); - } - - uint32_t present_mode_count{0}; - vkGetPhysicalDeviceSurfacePresentModesKHR(physical_device, surface, - &present_mode_count, nullptr); - if (present_mode_count > 0) { - details.present_modes.resize(present_mode_count); - vkGetPhysicalDeviceSurfacePresentModesKHR(physical_device, surface, - &present_mode_count, - details.present_modes.data()); +void Device::destroy() noexcept { + if (device_ != VK_NULL_HANDLE) { + vkDestroyDevice(device_, nullptr); + device_ = VK_NULL_HANDLE; } - return details; + physical_device_ = VK_NULL_HANDLE; + graphics_queue_ = VK_NULL_HANDLE; + present_queue_ = VK_NULL_HANDLE; + graphics_queue_family_index_ = 0; + present_queue_family_index_ = 0; + alloc_ = nullptr; } -[[nodiscard]] util::Result -pick_physical_device(VkInstance instance, VkSurfaceKHR surface, - const vector &required_extensions) { - - // TODO: use instance valid instead of manual check +util::Result Device::pick_physical_device_( + VkInstance instance, VkSurfaceKHR surface, + const std::vector &required_extensions) { QUARK_ENSURE(instance != VK_NULL_HANDLE, QUARK_ERR(util::Errc::InvalidArg, "instance is null")); QUARK_ENSURE(surface != VK_NULL_HANDLE, @@ -160,50 +115,40 @@ pick_physical_device(VkInstance instance, VkSurfaceKHR surface, QUARK_ENSURE(device_count > 0, QUARK_ERR(util::Errc::Unsupported, "No Vulkan physical devices found")); - vector devices(device_count); + std::vector devices(device_count); QUARK_VK_TRY( vkEnumeratePhysicalDevices(instance, &device_count, devices.data())); std::optional fallback; + auto enumerate = [&](VkPhysicalDevice physical_device) + -> util::Result> { + return enumerate_device_extensions_(physical_device); + }; for (const VkPhysicalDevice &physical_device : devices) { const auto graphics_queue_family_index = - find_graphics_queue_family(physical_device); + find_graphics_queue_family_or_error_(physical_device); if (!graphics_queue_family_index.has_value()) { continue; } - uint32_t queue_family_count{0}; - vkGetPhysicalDeviceQueueFamilyProperties(physical_device, - &queue_family_count, nullptr); - - std::optional present_queue_family_index; - for (auto index{0UZ}; index < queue_family_count; ++index) { - VkBool32 present_supported = VK_FALSE; - vkGetPhysicalDeviceSurfaceSupportKHR(physical_device, index, surface, - &present_supported); - if (present_supported == VK_TRUE) { - present_queue_family_index = index; - break; - } - } - + const auto present_queue_family_index = + find_present_queue_family_or_error_(physical_device, surface); if (!present_queue_family_index.has_value()) { continue; } - bool ok_exts = false; - QUARK_TRY_ASSIGN( - ok_exts, has_required_extensions(physical_device, required_extensions)); + std::vector props; + QUARK_TRY_ASSIGN(props, enumerate(physical_device)); - if (!ok_exts) { - continue; + bool ok_exts = true; + for (const char *name : required_extensions) { + if (!has_device_extension_props_(props, name)) { + ok_exts = false; + break; + } } - - const auto swapchain_support = - query_swapchain_support(physical_device, surface); - if (swapchain_support.formats.empty() || - swapchain_support.present_modes.empty()) { + if (!ok_exts) { continue; } @@ -228,23 +173,96 @@ pick_physical_device(VkInstance instance, VkSurfaceKHR surface, QUARK_ENSURE(fallback.has_value(), QUARK_ERR(util::Errc::Unsupported, "No suitable Vulkan physical device found")); - return *fallback; } -[[nodiscard]] util::Result> -build_device_extensions(VkPhysicalDevice physical_device, - const vector &required) { - vector props; - QUARK_TRY_ASSIGN(props, enumerate_device_extensions(physical_device)); +util::Result> +Device::enumerate_device_extensions_(VkPhysicalDevice physical_device) { + QUARK_ENSURE(physical_device != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidArg, "physical_device is null")); + + while (true) { + uint32_t count = 0; + QUARK_VK_TRY(vkEnumerateDeviceExtensionProperties( + physical_device, /*pLayerName=*/nullptr, &count, + /*pProperties=*/nullptr)); + + std::vector props(count); + + uint32_t written = count; + const VkResult r = vkEnumerateDeviceExtensionProperties( + physical_device, nullptr, &written, props.data()); + + if (r == VK_SUCCESS) { + props.resize(written); + return props; + } + + if (r != VK_INCOMPLETE) { + QUARK_FAIL(vk_error(r, "vkEnumerateDeviceExtensionProperties(data)")); + } + } +} + +bool Device::has_device_extension_props_( + const std::vector &props, + const char *extension_name) noexcept { + if (extension_name == nullptr || extension_name[0] == '\0') { + return false; + } + + for (const auto &p : props) { + if (std::strcmp(p.extensionName, extension_name) == 0) { + return true; + } + } + + return false; +} + +util::Result> +Device::build_device_extensions_(VkPhysicalDevice physical_device, + const std::vector &required) { + std::vector props; + QUARK_TRY_ASSIGN(props, enumerate_device_extensions_(physical_device)); + + std::vector enabled; + enabled.reserve(required.size() + 2); + + auto request_one = [&](const char *name) -> util::Status { + QUARK_ENSURE(name != nullptr, + QUARK_ERR(util::Errc::InvalidArg, "extension name is null")); + QUARK_ENSURE(name[0] != '\0', + QUARK_ERR(util::Errc::InvalidArg, "extension name is empty")); + + const bool supported = has_device_extension_props_(props, name); + QUARK_LOG_INFO("device extension requested: '{}' supported={}", name, + supported ? "true" : "false"); + + QUARK_ENSURE(supported, + QUARK_ERR(util::Errc::Unsupported, + "Required device extension unsupported {}", name)); + + if (std::ranges::none_of(enabled, [name](const char *e) { + return std::strcmp(e, name) == 0; + })) { + enabled.push_back(name); + QUARK_LOG_INFO("device extension enabled: '{}'", name); + } + QUARK_OK(); + }; - vector enabled = required; + for (const char *name : required) { + QUARK_TRY_STATUS(request_one(name)); + } #if defined(__APPLE__) // MoltenVK usually needs portability subset; constexpr const char *kPortabilitySubset = "VK_KHR_portability_subset"; - if (has_device_extension_props(props, kPortabilitySubset) && - !std::ranges::contains(enabled, kPortabilitySubset)) { + if (has_device_extension_props_(props, kPortabilitySubset) && + std::ranges::none_of(enabled, [](const char *e) { + return std::strcmp(e, kPortabilitySubset) == 0; + })) { enabled.push_back(kPortabilitySubset); QUARK_LOG_INFO("portability subset: enabled"); } @@ -253,81 +271,44 @@ build_device_extensions(VkPhysicalDevice physical_device, return enabled; } -} // namespace - -util::Status Device::create(const Device::CreateInfo &ci) { - destroy(); - - // TODO: use respective .valid()s - QUARK_ENSURE(ci.instance != VK_NULL_HANDLE, - QUARK_ERR(util::Errc::InvalidArg, - "Cannot create Vulkan device with null instance")); - QUARK_ENSURE(ci.surface != VK_NULL_HANDLE, - QUARK_ERR(util::Errc::InvalidArg, - "Cannot create Vulkan device with null surface")); - - DeviceSelection selection{}; - QUARK_TRY_ASSIGN(selection, pick_physical_device(ci.instance, ci.surface, - ci.required_extensions)); - - physical_device_ = selection.physical_device; - graphics_queue_family_index_ = selection.graphics_queue_family_index; - present_queue_family_index_ = selection.present_queue_family_index; - - constexpr float queue_priority{1.0F}; +util::Result +Device::find_graphics_queue_family_or_error_(VkPhysicalDevice physical_device) { + uint32_t queue_family_count{0}; + vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, + /*pQueueFamilyProperties=*/nullptr); - const std::set unique_queue_families{graphics_queue_family_index_, - present_queue_family_index_}; + std::vector queue_families(queue_family_count); + vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, + queue_families.data()); - vector queue_create_infos; - queue_create_infos.reserve(unique_queue_families.size()); - for (const uint32_t queue_family : unique_queue_families) { - VkDeviceQueueCreateInfo queue_create_info{}; - queue_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; - queue_create_info.queueFamilyIndex = queue_family; - queue_create_info.queueCount = 1; - queue_create_info.pQueuePriorities = &queue_priority; - queue_create_infos.push_back(queue_create_info); + for (auto index{0UZ}; index < queue_family_count; ++index) { + if ((queue_families[index].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0U) { + return index; + } } - vector enabled_device_extensions; - QUARK_TRY_ASSIGN( - enabled_device_extensions, - build_device_extensions(physical_device_, ci.required_extensions)); - - VkDeviceCreateInfo create_info{}; - create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; - create_info.pQueueCreateInfos = queue_create_infos.data(); - create_info.queueCreateInfoCount = - static_cast(queue_create_infos.size()); - create_info.enabledExtensionCount = - static_cast(enabled_device_extensions.size()); - create_info.ppEnabledExtensionNames = enabled_device_extensions.empty() - ? nullptr - : enabled_device_extensions.data(); - - VkDevice out = VK_NULL_HANDLE; - QUARK_VK_TRY(vkCreateDevice(physical_device_, &create_info, - /*pAllocator=*/nullptr, &out)); - device_ = out; - - vkGetDeviceQueue(device_, graphics_queue_family_index_, 0, &graphics_queue_); - vkGetDeviceQueue(device_, present_queue_family_index_, 0, &present_queue_); - - return {}; + QUARK_FAIL( + QUARK_ERR(util::Errc::Unsupported, "No graphics queue family found")); } -void Device::destroy() noexcept { - if (device_ != VK_NULL_HANDLE) { - vkDestroyDevice(device_, nullptr); - device_ = VK_NULL_HANDLE; +util::Result +Device::find_present_queue_family_or_error_(VkPhysicalDevice physical_device, + VkSurfaceKHR surface) { + uint32_t queue_family_count{0}; + vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, + /*pQueueFamilyProperties=*/nullptr); + + for (auto index{0UZ}; index < queue_family_count; ++index) { + VkBool32 present_supported = VK_FALSE; + vkGetPhysicalDeviceSurfaceSupportKHR(physical_device, index, surface, + &present_supported); + if (present_supported == VK_TRUE) { + return index; + } } - physical_device_ = VK_NULL_HANDLE; - graphics_queue_ = VK_NULL_HANDLE; - present_queue_ = VK_NULL_HANDLE; - graphics_queue_family_index_ = 0; - present_queue_family_index_ = 0; + QUARK_FAIL( + QUARK_ERR(util::Errc::Unsupported, "No present queue family found")); } -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/src/backend/vulkan/device/device_bundle.cpp b/src/backend/vulkan/device/device_bundle.cpp index 2ccd278..864f4ae 100644 --- a/src/backend/vulkan/device/device_bundle.cpp +++ b/src/backend/vulkan/device/device_bundle.cpp @@ -1,51 +1,82 @@ +#include #include -#include #include #include #include #include +#include +#include namespace quark::vk { -util::Status DeviceBundle::create(const Device::CreateInfo &ci) { +util::Status DeviceBundle::create(const DeviceBundle::CreateInfo &ci) { destroy(); - QUARK_TRY_ASSIGN(handle_, registry_.create(ci)); - return {}; + + QUARK_TRY_ASSIGN(handle_, registry_.create(ci.device)); + QUARK_OK(); } void DeviceBundle::destroy() noexcept { registry_.destroy(handle_); - handle_ = DeviceHandle{}; + handle_ = details::DeviceHandle{}; +} + +util::Status DeviceBundle::validate() const noexcept { + QUARK_ENSURE(registry_.alive(handle_), + QUARK_ERR(util::Errc::InvalidState, "device handle not alive")); + + return ::quark::vk::validate(view()); +} + +DeviceView DeviceBundle::view() const noexcept { + const details::Device *dev = registry_.get(handle_); + if (dev == nullptr) { + return {}; + } + + DeviceView v{}; + v.physical_device = dev->vk_physical_device(); + v.device = dev->vk_device(); + v.graphics_queue = dev->graphics_queue(); + v.graphics_queue_family_index = dev->graphics_queue_family_index(); + + return v; } VkPhysicalDevice DeviceBundle::vk_physical_device() const noexcept { - const Device *device = registry_.get(handle_); + const details::Device *device = registry_.get(handle_); return (device != nullptr) ? device->vk_physical_device() : VK_NULL_HANDLE; } VkDevice DeviceBundle::vk_device() const noexcept { - const Device *device = registry_.get(handle_); + const details::Device *device = registry_.get(handle_); return (device != nullptr) ? device->vk_device() : VK_NULL_HANDLE; } VkQueue DeviceBundle::graphics_queue() const noexcept { - const Device *device = registry_.get(handle_); + const details::Device *device = registry_.get(handle_); return (device != nullptr) ? device->graphics_queue() : VK_NULL_HANDLE; } VkQueue DeviceBundle::present_queue() const noexcept { - const Device *device = registry_.get(handle_); + const details::Device *device = registry_.get(handle_); return (device != nullptr) ? device->present_queue() : VK_NULL_HANDLE; } uint32_t DeviceBundle::graphics_queue_family_index() const noexcept { - const Device *device = registry_.get(handle_); + const details::Device *device = registry_.get(handle_); return (device != nullptr) ? device->graphics_queue_family_index() : 0; } uint32_t DeviceBundle::present_queue_family_index() const noexcept { - const Device *device = registry_.get(handle_); + const details::Device *device = registry_.get(handle_); return (device != nullptr) ? device->present_queue_family_index() : 0; } +details::DeviceCapabilities DeviceBundle::capabilities() const noexcept { + const details::Device *device = registry_.get(handle_); + return (device != nullptr) ? device->capabilities() + : details::DeviceCapabilities{}; +} + } // namespace quark::vk diff --git a/src/backend/vulkan/device/device_feature_chain_builder.cpp b/src/backend/vulkan/device/device_feature_chain_builder.cpp new file mode 100644 index 0000000..2cf7bce --- /dev/null +++ b/src/backend/vulkan/device/device_feature_chain_builder.cpp @@ -0,0 +1,146 @@ +#include +#include +#include +#include +#include + +namespace quark::vk::details { + +void DeviceFeatureChainBuilder::set_flag_(DeviceFeatureFlags &flags, + DeviceFeature feature, + bool value) noexcept { + const auto bit = static_cast(feature); + if (value) { + flags |= bit; + } else { + flags &= ~bit; + } +} + +void DeviceFeatureChainBuilder::reset_chain_() noexcept { + features2_ = {}; + features2_.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + features2_.pNext = nullptr; + + vk12_ = {}; + vk13_ = {}; + + vk12_.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + vk13_.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; + + features2_.pNext = &vk12_; + vk12_.pNext = &vk13_; + vk13_.pNext = nullptr; +} + +[[nodiscard]] bool DeviceFeatureChainBuilder::read_supported_feature_( + const FeatureDescriptor &desc) const noexcept { + switch (desc.wire_struct) { + case WireFeatureStruct::Vulkan12: + return vk12_.*(desc.vk12_member) == VK_TRUE; + + case WireFeatureStruct::Vulkan13: + return vk13_.*(desc.vk13_member) == VK_TRUE; + } + + return false; +} + +void DeviceFeatureChainBuilder::write_requested_feature_( + const FeatureDescriptor &desc, bool value) noexcept { + const VkBool32 vk_value = value ? VK_TRUE : VK_FALSE; + + switch (desc.wire_struct) { + case WireFeatureStruct::Vulkan12: + vk12_.*(desc.vk12_member) = vk_value; + return; + + case WireFeatureStruct::Vulkan13: + vk13_.*(desc.vk13_member) = vk_value; + return; + } +} + +util::Status +DeviceFeatureChainBuilder::query_supported(VkPhysicalDevice physical_device, + DeviceCapabilities &capabilities) { + QUARK_ENSURE(physical_device != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidArg, "physical_device is null")); + + reset_chain_(); + vkGetPhysicalDeviceFeatures2(physical_device, &features2_); + + supported_features_ = 0; + capabilities.supported_features = 0; + + for (const auto &desc : kFeatureDescriptors) { + const bool supported = read_supported_feature_(desc); + set_flag_(supported_features_, desc.feature, supported); + + if (supported) { + capabilities.supported_features |= + static_cast(desc.feature); + } + } + + QUARK_OK(); +} + +util::Status DeviceFeatureChainBuilder::maybe_enable_( + DeviceFeature feature, const char *name, bool supported, bool required, + bool preferred, bool &requested_out, DeviceCapabilities &capabilities) { + requested_out = false; + + if (!required && !preferred) { + QUARK_OK(); + } + QUARK_LOG_INFO("device feature requested: {} required={} supported={}", name, + required ? "true" : "false", supported ? "true" : "false"); + + if (!supported) { + QUARK_ENSURE(!required, + QUARK_ERR(util::Errc::Unsupported, + "Required device feature unsupported: {}", name)); + QUARK_LOG_INFO("device feature unavailable: {}", name); + QUARK_OK(); + } + + requested_out = true; + capabilities.enabled_features |= static_cast(feature); + QUARK_LOG_INFO("device feature enabled: {}", name); + QUARK_OK(); +} + +util::Status +DeviceFeatureChainBuilder::select_requested(DeviceFeatureFlags required, + DeviceFeatureFlags preferred, + DeviceCapabilities &capabilities) { + requested_features_ = 0; + capabilities.enabled_features = 0; + + for (const auto &desc : kFeatureDescriptors) { + const bool is_required = has_flag_(required, desc.feature); + const bool is_preferred = has_flag_(preferred, desc.feature); + const bool is_supported = has_flag_(supported_features_, desc.feature); + + bool request_this = false; + QUARK_TRY_STATUS(maybe_enable_(desc.feature, desc.name, is_supported, + is_required, is_preferred, request_this, + capabilities)); + + if (request_this) { + set_flag_(requested_features_, desc.feature, true); + } + } + + reset_chain_(); + + for (const auto &desc : kFeatureDescriptors) { + write_requested_feature_(desc, + has_flag_(requested_features_, desc.feature)); + } + + QUARK_OK(); +} + +} // namespace quark::vk::details diff --git a/src/backend/vulkan/device/device_registry.cpp b/src/backend/vulkan/device/device_registry.cpp index f1cc7b2..cb4edb5 100644 --- a/src/backend/vulkan/device/device_registry.cpp +++ b/src/backend/vulkan/device/device_registry.cpp @@ -5,7 +5,7 @@ #include #include -namespace quark::vk { +namespace quark::vk::details { bool DeviceRegistry::matches_(DeviceHandle handle, const Slot &s) noexcept { return handle.valid() && s.live && s.generation == handle.generation; @@ -94,4 +94,4 @@ const Device *DeviceRegistry::get(DeviceHandle handle) const noexcept { return &slots_[handle.index].device; } -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/src/backend/vulkan/device/device_view.cpp b/src/backend/vulkan/device/device_view.cpp new file mode 100644 index 0000000..bddbddc --- /dev/null +++ b/src/backend/vulkan/device/device_view.cpp @@ -0,0 +1,17 @@ +#include +#include +#include + +namespace quark::vk { + +util::Status validate(const DeviceView &view) noexcept { + QUARK_ENSURE(view.device != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidState, "VkDevice is null")); + QUARK_ENSURE(view.physical_device != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidState, "VkPhysicalDevice is null")); + QUARK_ENSURE(view.graphics_queue != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidState, "graphics queue is null")); + QUARK_OK(); +} + +} // namespace quark::vk diff --git a/src/backend/vulkan/frame/CMakeLists.txt b/src/backend/vulkan/frame/CMakeLists.txt new file mode 100644 index 0000000..3f773de --- /dev/null +++ b/src/backend/vulkan/frame/CMakeLists.txt @@ -0,0 +1,17 @@ +add_library(quark_backend_vulkan_frame STATIC + frame_cmd.cpp + frame_sync.cpp + frame_registry.cpp + frame_bundle.cpp +) +add_library(quark::backend::vulkan::frame ALIAS quark_backend_vulkan_frame) + +target_link_libraries(quark_backend_vulkan_frame + PUBLIC + quark::headers::vk +) + +target_include_directories(quark_backend_vulkan_frame + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/src/backend/vulkan/frame/frame_bundle.cpp b/src/backend/vulkan/frame/frame_bundle.cpp new file mode 100644 index 0000000..3ff4a89 --- /dev/null +++ b/src/backend/vulkan/frame/frame_bundle.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include +#include +#include + +namespace quark::vk { + +util::Status FrameBundle::create(const CreateInfo &ci) { + destroy(); + + QUARK_TRY_ASSIGN(handle_, registry_.create(ci)); + QUARK_OK(); +} + +void FrameBundle::destroy() noexcept { + if (!handle_.valid()) { + return; + } + + registry_.clear(); + handle_ = details::FrameHandle{}; +} + +FrameView FrameBundle::view() noexcept { + FrameView v{}; + v.handle = handle_; + v.cmd = registry_.cmd(handle_); + v.sync = registry_.sync(handle_); + return v; +} + +ConstFrameView FrameBundle::view() const noexcept { + ConstFrameView v{}; + v.handle = handle_; + v.cmd = registry_.cmd(handle_); + v.sync = registry_.sync(handle_); + return v; +} + +details::FrameCmd *FrameBundle::cmd() noexcept { + return registry_.cmd(handle_); +} + +const details::FrameCmd *FrameBundle::cmd() const noexcept { + return registry_.cmd(handle_); +} + +details::FrameSync *FrameBundle::sync() noexcept { + return registry_.sync(handle_); +} + +const details::FrameSync *FrameBundle::sync() const noexcept { + return registry_.sync(handle_); +} + +void FrameBundle::retire(uint64_t retire_at) noexcept { + if (!handle_.valid()) { + return; + } + + registry_.destory(handle_, retire_at); + handle_ = details::FrameHandle{}; +} + +} // namespace quark::vk diff --git a/src/backend/vulkan/frame/frame_cmd.cpp b/src/backend/vulkan/frame/frame_cmd.cpp new file mode 100644 index 0000000..2b666b4 --- /dev/null +++ b/src/backend/vulkan/frame/frame_cmd.cpp @@ -0,0 +1,80 @@ +#include +#include +#include +#include + +namespace quark::vk::details { + +util::Status FrameCmd::create(const CreateInfo &ci) { + destroy(); + + QUARK_TRY_VALIDATE(validate(ci.device)); + + vk_device_ = ci.device.device; + graphics_qfi_ = ci.device.graphics_queue_family_index; + alloc_ = ci.allocator; + + QUARK_ENSURE(vk_device_ != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidArg, "VkDevice is null")); + + VkCommandPoolCreateInfo pool_info{}; + pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + pool_info.flags = ci.pool_flags; + pool_info.queueFamilyIndex = graphics_qfi_; + + QUARK_VK_TRY(vkCreateCommandPool(vk_device_, &pool_info, alloc_, &pool_)); + + QUARK_TRY_STATUS(resize(ci.buffer_count)); + QUARK_OK(); +} + +void FrameCmd::free_buffers_() noexcept { + if (!buffers_.empty() && vk_device_ != VK_NULL_HANDLE && + pool_ != VK_NULL_HANDLE) { + vkFreeCommandBuffers(vk_device_, pool_, + static_cast(buffers_.size()), + buffers_.data()); + buffers_.clear(); + } +} + +void FrameCmd::destroy() noexcept { + free_buffers_(); + + if (vk_device_ != VK_NULL_HANDLE && pool_ != VK_NULL_HANDLE) { + vkDestroyCommandPool(vk_device_, pool_, alloc_); + } + + pool_ = VK_NULL_HANDLE; + vk_device_ = VK_NULL_HANDLE; + graphics_qfi_ = 0; + alloc_ = nullptr; + buffers_.clear(); +} + +util::Status FrameCmd::resize(uint32_t new_count) { + QUARK_ENSURE(vk_device_ != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidState, "device is null")); + QUARK_ENSURE(pool_ != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidState, "pool is null")); + + if (new_count == static_cast(buffers_.size())) { + QUARK_OK(); + } + + free_buffers_(); + + buffers_.assign(new_count, VK_NULL_HANDLE); + + VkCommandBufferAllocateInfo alloc_info{}; + alloc_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + alloc_info.commandPool = pool_; + alloc_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + alloc_info.commandBufferCount = new_count; + + QUARK_VK_TRY( + vkAllocateCommandBuffers(vk_device_, &alloc_info, buffers_.data())); + QUARK_OK(); +} + +} // namespace quark::vk::details diff --git a/src/backend/vulkan/frame/frame_registry.cpp b/src/backend/vulkan/frame/frame_registry.cpp new file mode 100644 index 0000000..34779b2 --- /dev/null +++ b/src/backend/vulkan/frame/frame_registry.cpp @@ -0,0 +1,183 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace quark::vk::details { + +bool FrameRegistry::matches_(FrameHandle handle, const Slot &slot) noexcept { + return handle.valid() && slot.live && slot.generation == handle.generation; +} + +void FrameRegistry::destroy_retired_frame_(void *ctx) noexcept { + auto *payload = static_cast(ctx); + if (payload == nullptr) { + return; + } + + payload->cmd.destroy(); + payload->sync.destroy(); +} + +void FrameRegistry::cleanup_retired_frame_(void *ctx) noexcept { + auto *payload = static_cast(ctx); + delete payload; +} + +util::Result FrameRegistry::create(const CreateInfo &ci) { + QUARK_TRY_VALIDATE(validate(ci.device)); + + QUARK_ENSURE(ci.retire_queue != nullptr, + QUARK_ERR(util::Errc::InvalidArg, "retire_queue is null")); + + QUARK_ENSURE( + ci.frames_in_flight > 0, + QUARK_ERR(util::Errc::InvalidArg, "frames_in_flight must be > 0")); + + retire_queue_ = ci.retire_queue; + + uint32_t idx = 0; + if (!free_.empty()) { + idx = free_.back(); + free_.pop_back(); + } else { + idx = static_cast(slots_.size()); + slots_.push_back(Slot{}); + } + + Slot &slot = slots_[idx]; + + if (slot.live) { + QUARK_LOG_WARN("Slot reused"); + slot.cmd.destroy(); + slot.sync.destroy(); + slot.live = false; + ++slot.generation; + } + + FrameCmd::CreateInfo cmd_ci{}; + cmd_ci.device = ci.device; + cmd_ci.buffer_count = ci.cmd_buffer_count; + cmd_ci.pool_flags = ci.cmd_pool_flags; + cmd_ci.allocator = ci.allocator; + + FrameSync::CreateInfo sync_ci{}; + sync_ci.device = ci.device; + sync_ci.frames_in_flight = ci.frames_in_flight; + sync_ci.allocator = ci.allocator; + sync_ci.frames_idle_on_create = ci.frames_idle_on_create; + + QUARK_TRY_STATUS(slot.cmd.create(cmd_ci)); + QUARK_TRY_STATUS(slot.sync.create(sync_ci)); + + slot.live = true; + + return FrameHandle{.index = idx, .generation = slot.generation}; +} + +void FrameRegistry::destory(FrameHandle handle, uint64_t retire_at) noexcept { + if (retire_queue_ == nullptr) { + return; + } + + if (!handle.valid() || handle.index >= slots_.size()) { + return; + } + + Slot &slot = slots_[handle.index]; + if (!matches_(handle, slot)) { + return; + } + + auto *payload = new (std::nothrow) RetiredFramePayload{}; + if (payload == nullptr) { + QUARK_LOG_WARN("Payload is erroneously nullptr, falling back"); + slot.cmd.destroy(); + slot.sync.destroy(); + slot.live = false; + ++slot.generation; + free_.push_back(handle.index); + return; + } + + payload->cmd = std::move(slot.cmd); + payload->sync = std::move(slot.sync); + + slot.live = false; + ++slot.generation; + free_.push_back(handle.index); + + RetirementQueue::Task task{}; + task.fn = &FrameRegistry::destroy_retired_frame_; + task.cleanup = &FrameRegistry::cleanup_retired_frame_; + task.ctx = payload; + + auto res = retire_queue_->enqueue(retire_at, task); + if (!res) { + destroy_retired_frame_(payload); + cleanup_retired_frame_(payload); + } +} + +void FrameRegistry::clear() noexcept { + for (auto &slot : slots_) { + if (slot.live) { + slot.cmd.destroy(); + slot.sync.destroy(); + slot.live = false; + ++slot.generation; + } + } + + slots_.clear(); + free_.clear(); + retire_queue_ = nullptr; +} + +bool FrameRegistry::alive(FrameHandle handle) const noexcept { + if (!handle.valid() || handle.index >= slots_.size()) { + return false; + } + + return matches_(handle, slots_[handle.index]); +} + +FrameCmd *FrameRegistry::cmd(FrameHandle handle) noexcept { + if (!alive(handle)) { + return nullptr; + } + + return &slots_[handle.index].cmd; +} + +const FrameCmd *FrameRegistry::cmd(FrameHandle handle) const noexcept { + if (!alive(handle)) { + return nullptr; + } + + return &slots_[handle.index].cmd; +} + +FrameSync *FrameRegistry::sync(FrameHandle handle) noexcept { + if (!alive(handle)) { + return nullptr; + } + + return &slots_[handle.index].sync; +} + +const FrameSync *FrameRegistry::sync(FrameHandle handle) const noexcept { + if (!alive(handle)) { + return nullptr; + } + + return &slots_[handle.index].sync; +} + +} // namespace quark::vk::details diff --git a/src/backend/vulkan/frame/frame_sync.cpp b/src/backend/vulkan/frame/frame_sync.cpp new file mode 100644 index 0000000..adba12e --- /dev/null +++ b/src/backend/vulkan/frame/frame_sync.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include + +namespace quark::vk::details { + +util::Status FrameSync::create(const CreateInfo &ci) { + destroy(); + + QUARK_TRY_VALIDATE(validate(ci.device)); + + QUARK_ENSURE( + ci.frames_in_flight > 0, + QUARK_ERR(util::Errc::InvalidArg, "frames_in_flight must be > 0")); + + vk_device_ = ci.device.device; + alloc_ = ci.allocator; + frames_idle_on_create_ = ci.frames_idle_on_create; + + QUARK_TRY_STATUS(create_per_frame_(ci.frames_in_flight)); + + QUARK_OK(); +} + +void FrameSync::destroy() noexcept { + destroy_per_frame_(); + + vk_device_ = VK_NULL_HANDLE; + alloc_ = nullptr; + frames_idle_on_create_ = true; +} + +util::Status FrameSync::resize(uint32_t new_frames_in_flight) { + QUARK_ENSURE(vk_device_ != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidState, "not created")); + QUARK_ENSURE( + new_frames_in_flight > 0, + QUARK_ERR(util::Errc::InvalidArg, "frames_in_flight must be > 0")); + + destroy_per_frame_(); + return create_per_frame_(new_frames_in_flight); +} + +util::Status FrameSync::create_per_frame_(uint32_t count) { + frames_.assign(count, PerFrame{}); + + if (frames_idle_on_create_) { + for (auto &frame : frames_) { + frame.in_flight_value = 0; // idle sentinel + } + } + + VkSemaphoreCreateInfo semaphore_info{}; + semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + for (uint32_t i = 0; i < count; ++i) { + QUARK_VK_TRY(vkCreateSemaphore(vk_device_, &semaphore_info, alloc_, + &frames_[i].image_available)); + QUARK_VK_TRY(vkCreateSemaphore(vk_device_, &semaphore_info, alloc_, + &frames_[i].render_finished)); + } + + QUARK_OK(); +} + +void FrameSync::destroy_per_frame_() noexcept { + if (vk_device_ == VK_NULL_HANDLE) { + frames_.clear(); + return; + } + + for (auto &frame : frames_) { + if (frame.image_available != VK_NULL_HANDLE) { + vkDestroySemaphore(vk_device_, frame.image_available, alloc_); + frame.image_available = VK_NULL_HANDLE; + } + + if (frame.render_finished != VK_NULL_HANDLE) { + vkDestroySemaphore(vk_device_, frame.render_finished, alloc_); + frame.render_finished = VK_NULL_HANDLE; + } + + frame.in_flight_value = 0; + } + + frames_.clear(); +} + +} // namespace quark::vk::details diff --git a/src/backend/vulkan/instance/debug_messenger.cpp b/src/backend/vulkan/instance/debug_messenger.cpp index 23744c9..14984ec 100644 --- a/src/backend/vulkan/instance/debug_messenger.cpp +++ b/src/backend/vulkan/instance/debug_messenger.cpp @@ -3,7 +3,7 @@ #include #include -namespace quark::vk { +namespace quark::vk::details { namespace { @@ -41,11 +41,11 @@ util::Status DebugMessenger::create(VkInstance instance, const CreateInfo &ci) { info.pUserData = ci.user_data; VkDebugUtilsMessengerEXT out = VK_NULL_HANDLE; - QUARK_VK_TRY(create_fn(instance, &info, nullptr, &out)); + QUARK_VK_TRY(create_fn(instance, &info, alloc_, &out)); instance_ = instance; messenger_ = out; - return {}; + QUARK_OK(); } void DebugMessenger::destroy() noexcept { @@ -65,4 +65,4 @@ void DebugMessenger::destroy() noexcept { instance_ = VK_NULL_HANDLE; } -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/src/backend/vulkan/instance/instance.cpp b/src/backend/vulkan/instance/instance.cpp index 1b6a45b..6affa9e 100644 --- a/src/backend/vulkan/instance/instance.cpp +++ b/src/backend/vulkan/instance/instance.cpp @@ -7,7 +7,7 @@ #include #include -namespace quark::vk { +namespace quark::vk::details { namespace { @@ -148,7 +148,7 @@ util::Status Instance::build_instance_extensions_( "debug utils", /*required=*/true)); } - return {}; + QUARK_OK(); } util::Status Instance::create(const CreateInfo &ci) { @@ -212,14 +212,14 @@ util::Status Instance::create(const CreateInfo &ci) { } VkInstance instance = VK_NULL_HANDLE; - QUARK_VK_TRY(vkCreateInstance(&create, /*pAllocator=*/nullptr, &instance)); + QUARK_VK_TRY(vkCreateInstance(&create, alloc_, &instance)); instance_ = instance; if (ci.enable_debug_messenger && enable_debug_utils) { QUARK_TRY_STATUS(debug_messenger_.create(instance_, dbg)); } - return {}; + QUARK_OK(); } void Instance::destroy() noexcept { @@ -231,4 +231,4 @@ void Instance::destroy() noexcept { } } -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/src/backend/vulkan/instance/instance_bundle.cpp b/src/backend/vulkan/instance/instance_bundle.cpp index e627826..ca02a30 100644 --- a/src/backend/vulkan/instance/instance_bundle.cpp +++ b/src/backend/vulkan/instance/instance_bundle.cpp @@ -8,25 +8,26 @@ namespace quark::vk { -util::Status InstanceBundle::create(const Instance::CreateInfo &ci) { +util::Status InstanceBundle::create(const CreateInfo &ci) { destroy(); - QUARK_TRY_ASSIGN(handle_, registry_.create(ci)); + QUARK_TRY_ASSIGN(handle_, registry_.create(ci.instance)); return {}; } void InstanceBundle::destroy() noexcept { registry_.destroy(handle_); - handle_ = InstanceHandle{}; + handle_ = details::InstanceHandle{}; } VkInstance InstanceBundle::vk_instance() const noexcept { - const Instance *instance = registry_.get(handle_); + const details::Instance *instance = registry_.get(handle_); return (instance != nullptr) ? instance->handle() : VK_NULL_HANDLE; } -const DebugMessenger *InstanceBundle::debug_messenger() const noexcept { - const Instance *instance = registry_.get(handle_); +const details::DebugMessenger * +InstanceBundle::debug_messenger() const noexcept { + const details::Instance *instance = registry_.get(handle_); if (instance == nullptr) { return nullptr; } diff --git a/src/backend/vulkan/instance/instance_registry.cpp b/src/backend/vulkan/instance/instance_registry.cpp index 337bf1c..3a50197 100644 --- a/src/backend/vulkan/instance/instance_registry.cpp +++ b/src/backend/vulkan/instance/instance_registry.cpp @@ -6,7 +6,7 @@ #include #include -namespace quark::vk { +namespace quark::vk::details { bool InstanceRegistry::matches_(InstanceHandle handle, const Slot &s) noexcept { return handle.valid() && s.live && s.generation == handle.generation; @@ -95,4 +95,4 @@ const Instance *InstanceRegistry::get(InstanceHandle handle) const noexcept { return &slots_[handle.index].instance; } -} // namespace quark::vk +} // namespace quark::vk::details diff --git a/src/backend/vulkan/presentation/CMakeLists.txt b/src/backend/vulkan/presentation/CMakeLists.txt index 1f555d7..631aae6 100644 --- a/src/backend/vulkan/presentation/CMakeLists.txt +++ b/src/backend/vulkan/presentation/CMakeLists.txt @@ -1,16 +1,17 @@ -add_library(quark_backend_vulkan_presentation STATIC +add_library(quark_backend_vulkan_presentation STATIC + presenter_bundle.cpp swapchain.cpp ) add_library(quark::backend::vulkan::presentation ALIAS quark_backend_vulkan_presentation) target_link_libraries(quark_backend_vulkan_presentation PUBLIC - quark::headers - PRIVATE + quark::headers::vk + PRIVATE Vulkan::Vulkan ) target_include_directories(quark_backend_vulkan_presentation - PRIVATE + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ) diff --git a/src/backend/vulkan/presentation/presenter_bundle.cpp b/src/backend/vulkan/presentation/presenter_bundle.cpp new file mode 100644 index 0000000..89a8458 --- /dev/null +++ b/src/backend/vulkan/presentation/presenter_bundle.cpp @@ -0,0 +1,103 @@ +#include +#include +#include +#include + +namespace quark::vk { + +namespace { + +util::Result create_surface(VkInstance instance, + const platform::IWindow *window) { + QUARK_ENSURE(instance != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidArg, + "PresenterBundle::create_surface: instance is null")); + QUARK_ENSURE(window != nullptr, + QUARK_ERR(util::Errc::InvalidArg, + "PresenterBundle::create_surface: window is null")); + + const auto *source = platform::query(*window); + QUARK_ENSURE( + source != nullptr, + QUARK_ERR(util::Errc::Unsupported, + "PresenterBundle::create_surface: window does not expose " + "IVulkanSurfaceSource")); + + VkSurfaceKHR surface = source->create_surface(instance); + QUARK_ENSURE( + surface != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::ApiError, + "PresenterBundle::create_surface: create_surface returned " + "VK_NULL_HANDLE")); + + return surface; +} + +void destroy_surface(VkInstance instance, VkSurfaceKHR &surface) noexcept { + if (instance == VK_NULL_HANDLE || surface == VK_NULL_HANDLE) { + return; + } + + vkDestroySurfaceKHR(instance, surface, nullptr); + surface = VK_NULL_HANDLE; +} + +} // namespace + +util::Status PresenterBundle::create(const CreateInfo &ci) { + destroy(); + + create_info_ = ci; + + QUARK_TRY_ASSIGN(surface_, create_surface(ci.instance, ci.window)); + + Swapchain::CreateInfo sci{}; + sci.physical_device = ci.physical_device; + sci.device = ci.device; + sci.surface = surface_; + sci.graphics_queue_family_index = ci.graphics_queue_family_index; + sci.present_queue_family_index = ci.present_queue_family_index; + sci.window = ci.window; + sci.old_swapchain = VK_NULL_HANDLE; + sci.preferred_present_mode = ci.preferred_present_mode; + sci.preferred_format = ci.preferred_format; + sci.preferred_color_space = ci.preferred_color_space; + + if (auto res = swapchain_.create(sci); !res) { + destroy_surface(ci.instance, surface_); + return util::unexpected(std::move(res.error())); + } + + QUARK_OK(); +} + +util::Status PresenterBundle::recreate_swapchain() { + VkSwapchainKHR old_handle = swapchain_.handle(); + + Swapchain::CreateInfo sci{}; + sci.physical_device = create_info_.physical_device; + sci.device = create_info_.device; + sci.surface = surface_; + sci.graphics_queue_family_index = create_info_.graphics_queue_family_index; + sci.present_queue_family_index = create_info_.present_queue_family_index; + sci.window = create_info_.window; + sci.old_swapchain = old_handle; + sci.preferred_present_mode = create_info_.preferred_present_mode; + sci.preferred_format = create_info_.preferred_format; + sci.preferred_color_space = create_info_.preferred_color_space; + + Swapchain next_swapchain; + QUARK_TRY_STATUS(next_swapchain.create(sci)); + swapchain_ = std::move(next_swapchain); + QUARK_OK(); +} + +void PresenterBundle::destroy_swapchain() noexcept { swapchain_.reset(); } + +void PresenterBundle::destroy() noexcept { + destroy_swapchain(); + destroy_surface(create_info_.instance, surface_); + create_info_ = CreateInfo{}; +} + +} // namespace quark::vk \ No newline at end of file diff --git a/src/backend/vulkan/presentation/swapchain.cpp b/src/backend/vulkan/presentation/swapchain.cpp index d3cab85..f91a2df 100644 --- a/src/backend/vulkan/presentation/swapchain.cpp +++ b/src/backend/vulkan/presentation/swapchain.cpp @@ -1,7 +1,10 @@ #include +#include +#include #include +#include #include -#include +#include #include #include @@ -93,41 +96,56 @@ VkExtent2D choose_extent(const platform::IWindow &window, return actual_extent; } -void throw_if_vk(VkResult result, const char *what) { - if (result != VK_SUCCESS) { - throw std::runtime_error(what); - } -} - } // namespace -void Swapchain::create(const CreateInfo &ci) { - reset(); +Swapchain::Swapchain(Swapchain &&other) noexcept + : device_(std::exchange(other.device_, VK_NULL_HANDLE)), + swapchain_(std::exchange(other.swapchain_, VK_NULL_HANDLE)), + image_format_(std::exchange(other.image_format_, VK_FORMAT_UNDEFINED)), + extent_(std::exchange(other.extent_, VkExtent2D{})), + images_(std::move(other.images_)), + image_views_(std::move(other.image_views_)) {} - // TODO: Replace with device valid() - if (ci.physical_device == VK_NULL_HANDLE) { - throw std::runtime_error("Swapchain::create: physical_device is null"); +Swapchain &Swapchain::operator=(Swapchain &&other) noexcept { + if (this == &other) { + return *this; } - if (ci.device == VK_NULL_HANDLE) { - throw std::runtime_error("Swapchain::create: device is null"); - } + reset(); - if (ci.surface == VK_NULL_HANDLE) { - throw std::runtime_error("Swapchain::create: surface is null"); - } + device_ = std::exchange(other.device_, VK_NULL_HANDLE); + swapchain_ = std::exchange(other.swapchain_, VK_NULL_HANDLE); + image_format_ = std::exchange(other.image_format_, VK_FORMAT_UNDEFINED); + extent_ = std::exchange(other.extent_, VkExtent2D{}); + images_ = std::move(other.images_); + image_views_ = std::move(other.image_views_); + return *this; +} + +util::Status Swapchain::create(const CreateInfo &ci) { + reset(); + // TODO: Replace with device valid() + QUARK_ENSURE(ci.physical_device != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidArg, + "Swapchain::create: physical_device is null")); + QUARK_ENSURE( + ci.device != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidArg, "Swapchain::create: device is null")); + QUARK_ENSURE( + ci.surface != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidArg, "Swapchain::create: surface is null")); // TODO: Replace with window valid - if (ci.window == nullptr) { - throw std::runtime_error("Swapchain::create: window is null"); - } + QUARK_ENSURE( + ci.window != nullptr, + QUARK_ERR(util::Errc::InvalidArg, "Swapchain::create: window is null")); device_ = ci.device; const SupportDetails support = query_support(ci.physical_device, ci.surface); if (support.formats.empty() || support.present_modes.empty()) { - throw std::runtime_error( - "Swapchain::create: swapchain support is incomplete"); + QUARK_FAIL(QUARK_ERR(util::Errc::Unsupported, + "Swapchain::create: swapchain support is incomplete")); } const VkSurfaceFormatKHR surface_format = choose_format( @@ -170,9 +188,8 @@ void Swapchain::create(const CreateInfo &ci) { VkResult r = vkCreateSwapchainKHR(ci.device, &sci, /*pAllocator=*/nullptr, &swapchain_); - if (r != VK_SUCCESS) { - throw std::runtime_error("vkCreateSwapchainKHR failed"); - } + QUARK_ENSURE(r == VK_SUCCESS, + QUARK_ERR(util::Errc::ApiError, "vkCreateSwapchainKHR failed")); vkGetSwapchainImagesKHR(ci.device, swapchain_, &image_count, /*pSwapchainImages=*/nullptr); @@ -197,10 +214,11 @@ void Swapchain::create(const CreateInfo &ci) { r = vkCreateImageView(ci.device, &ivci, /*pAllocator=*/nullptr, &image_views_[index]); - if (r != VK_SUCCESS) { - throw std::runtime_error("vkCreateImageView failed"); - } + QUARK_ENSURE(r == VK_SUCCESS, + QUARK_ERR(util::Errc::ApiError, "vkCreateImageView failed")); } + + QUARK_OK(); } void Swapchain::reset() noexcept { diff --git a/src/backend/vulkan/sync/CMakeLists.txt b/src/backend/vulkan/sync/CMakeLists.txt new file mode 100644 index 0000000..f9c2cb4 --- /dev/null +++ b/src/backend/vulkan/sync/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(quark_backend_vulkan_sync STATIC gpu_timeline.cpp) +add_library(quark::backend::vulkan::sync ALIAS quark_backend_vulkan_sync) + +target_link_libraries(quark_backend_vulkan_sync PUBLIC quark::headers::vk) + +target_include_directories(quark_backend_vulkan_sync + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/backend/vulkan/sync/gpu_timeline.cpp b/src/backend/vulkan/sync/gpu_timeline.cpp new file mode 100644 index 0000000..b60b713 --- /dev/null +++ b/src/backend/vulkan/sync/gpu_timeline.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include + +namespace quark::vk { + +util::Status GpuTimeline::create(const CreateInfo &ci) { + destroy(); + + QUARK_TRY_VALIDATE(validate(ci.device)); + QUARK_ENSURE(ci.initial_value < UINT64_MAX, + QUARK_ERR(util::Errc::InvalidArg, "initial_value is invalid")); + QUARK_ENSURE( + ci.first_signal_value == 0, + QUARK_ERR(util::Errc::InvalidArg, "first_signal_value must be = 0")); + + vk_device_ = ci.device.device; + alloc_ = ci.allocator; + + VkSemaphoreTypeCreateInfo type_info{}; + type_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; + type_info.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + type_info.initialValue = ci.initial_value; + + VkSemaphoreCreateInfo sem_ci{}; + sem_ci.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + sem_ci.pNext = &type_info; + + QUARK_VK_TRY(vkCreateSemaphore(vk_device_, &sem_ci, alloc_, &timeline_)); + + next_value_ = ci.first_signal_value; + QUARK_OK(); +} + +void GpuTimeline::destroy() noexcept { + if (vk_device_ != VK_NULL_HANDLE && timeline_ != VK_NULL_HANDLE) { + vkDestroySemaphore(vk_device_, timeline_, alloc_); + } + + timeline_ = VK_NULL_HANDLE; + vk_device_ = VK_NULL_HANDLE; + alloc_ = nullptr; + next_value_ = 1; +} + +util::Result GpuTimeline::completed_value() const noexcept { + QUARK_ENSURE(vk_device_ != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidState, "not created")); + QUARK_ENSURE( + timeline_ != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidState, "timeline semaphore is null")); + + uint64_t v = 0; + QUARK_VK_TRY(vkGetSemaphoreCounterValue(vk_device_, timeline_, &v)); + return v; +} + +util::Status GpuTimeline::wait(uint64_t value, uint64_t timeout_ns) const { + QUARK_ENSURE(vk_device_ != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidState, "not created")); + QUARK_ENSURE( + timeline_ != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidState, "timeline semaphore is null")); + + if (value == 0) { + QUARK_OK(); + } + + VkSemaphoreWaitInfo wait_info{}; + wait_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + wait_info.semaphoreCount = 1; + wait_info.pSemaphores = &timeline_; + wait_info.pValues = &value; + + // TODO: make better pattern for different VK result pattern matching + const VkResult r = vkWaitSemaphores(vk_device_, &wait_info, timeout_ns); + if (r == VK_TIMEOUT) { + QUARK_FAIL(QUARK_ERR(util::Errc::Timeout, "vkWaitSemaphores timeout")); + } + QUARK_ENSURE(r == VK_SUCCESS, ::quark::vk::vk_error(r, "vkWaitSemaphores")); + + QUARK_OK(); +} + +} // namespace quark::vk diff --git a/src/backend/vulkan/vulkan_context.cpp b/src/backend/vulkan/vulkan_context.cpp index 98e6777..6d224aa 100644 --- a/src/backend/vulkan/vulkan_context.cpp +++ b/src/backend/vulkan/vulkan_context.cpp @@ -1,25 +1,83 @@ #include "vulkan_context.hpp" +// TODO: move test to testing system when possible +#include "quark/engine/retire/retirement_queue.hpp" + #include #include +#include +#include #include #include -#include -#include #include +#include #include #include #include #include #include -#include +#include #include #include -#include +#include using std::array; using std::vector; +// TODO: move test to testing system when possible +namespace { + +struct RetireTestPayload { + uint64_t id = 0; + uint64_t retire_at = 0; +}; + +void retire_test_run(void *ctx) noexcept { + auto *payload = static_cast(ctx); + if (payload == nullptr) { + return; + } + + QUARK_LOG_INFO_MOD("retire.test", "drained test payload id={} retire_at={}", + payload->id, payload->retire_at); +} + +void retire_test_cleanup(void *ctx) noexcept { + auto *payload = static_cast(ctx); + delete payload; +} + +[[maybe_unused]] util::Status +enqueue_retire_test(quark::vk::RetirementQueue &queue, uint64_t retire_at, + uint64_t id) { + auto *payload = new (std::nothrow) RetireTestPayload{ + .id = id, + .retire_at = retire_at, + }; + + QUARK_ENSURE(payload != nullptr, + QUARK_ERR(util::Errc::OutOfMemory, + "failed to allocate RetireTestPayload")); + + quark::vk::RetirementQueue::Task task{}; + task.fn = &retire_test_run; + task.cleanup = &retire_test_cleanup; + task.ctx = payload; + + QUARK_LOG_INFO_MOD("retire.test", "enqueue test payload id={} retire_at={}", + id, retire_at); + + auto res = queue.enqueue(retire_at, task); + if (!res) { + retire_test_cleanup(payload); + return util::unexpected(std::move(res.error())); + } + + QUARK_OK(); +} + +} // namespace + namespace { constexpr int kWindowWidth{1280}; @@ -39,11 +97,31 @@ constexpr array kValidationLayers{ constexpr const char *kSwapchainExtension = VK_KHR_SWAPCHAIN_EXTENSION_NAME; -struct SwapchainSupportDetails { - VkSurfaceCapabilitiesKHR capabilities{}; - vector formats; - vector present_modes; -}; +auto api_version_at_least(uint32_t version, uint32_t major, uint32_t minor) + -> bool { + if (VK_VERSION_MAJOR(version) != major) { + return VK_VERSION_MAJOR(version) > major; + } + + return VK_VERSION_MINOR(version) >= minor; +} + +auto choose_instance_api_version() -> uint32_t { + uint32_t loader_version = VK_API_VERSION_1_0; +#if defined(VK_VERSION_1_1) + vkEnumerateInstanceVersion(&loader_version); +#endif + + if (api_version_at_least(loader_version, 1, 3)) { + return VK_API_VERSION_1_3; + } + + if (api_version_at_least(loader_version, 1, 2)) { + return VK_API_VERSION_1_2; + } + + return VK_API_VERSION_1_0; +} auto check_validation_layer_support() -> bool { uint32_t layer_count{0}; @@ -66,104 +144,35 @@ auto check_validation_layer_support() -> bool { return true; } -auto query_swapchain_support(VkPhysicalDevice device, VkSurfaceKHR surface) - -> SwapchainSupportDetails { - SwapchainSupportDetails details; +auto create_probe_surface(VkInstance instance, + const quark::platform::IWindow &window) + -> util::Result { + QUARK_ENSURE( + instance != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::InvalidArg, "probe surface instance is null")); - vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, - &details.capabilities); - - uint32_t format_count{0}; - vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &format_count, nullptr); - if (format_count > 0) { - details.formats.resize(format_count); - vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &format_count, - details.formats.data()); - } - - uint32_t present_mode_count{0}; - vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, - &present_mode_count, nullptr); - if (present_mode_count > 0) { - details.present_modes.resize(present_mode_count); - vkGetPhysicalDeviceSurfacePresentModesKHR( - device, surface, &present_mode_count, details.present_modes.data()); - } - - return details; -} - -auto choose_swapchain_surface_format( - const vector &available_formats) -> VkSurfaceFormatKHR { - for (const VkSurfaceFormatKHR &surface_format : available_formats) { - if (surface_format.format == VK_FORMAT_B8G8R8A8_SRGB && - surface_format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { - return surface_format; - } - } - return available_formats.front(); -} - -auto choose_swapchain_present_mode( - const vector &available_present_modes) - -> VkPresentModeKHR { - for (const VkPresentModeKHR present_mode : available_present_modes) { - if (present_mode == VK_PRESENT_MODE_MAILBOX_KHR) { - return present_mode; - } - } - - return VK_PRESENT_MODE_FIFO_KHR; -} - -auto choose_swapchain_extent(const quark::platform::IWindow &window, - const VkSurfaceCapabilitiesKHR &capabilities) - -> VkExtent2D { - if (capabilities.currentExtent.width != - std::numeric_limits::max()) { - return capabilities.currentExtent; - } + const auto *source = + quark::platform::query(window); + QUARK_ENSURE(source != nullptr, + QUARK_ERR(util::Errc::Unsupported, + "Window does not provide IVulkanSurfaceSource")); - int width{0}; - int height{0}; - window.framebuffer_size(width, height); + VkSurfaceKHR surface = source->create_surface(instance); + QUARK_ENSURE(surface != VK_NULL_HANDLE, + QUARK_ERR(util::Errc::ApiError, + "probe surface creation returned VK_NULL_HANDLE")); - VkExtent2D actual_extent{ - .width = static_cast(width), - .height = static_cast(height), - }; - - actual_extent.width = - std::clamp(actual_extent.width, capabilities.minImageExtent.width, - capabilities.maxImageExtent.width); - actual_extent.height = - std::clamp(actual_extent.height, capabilities.minImageExtent.height, - capabilities.maxImageExtent.height); - return actual_extent; + return surface; } -auto vk_result_to_string(const VkResult result) -> std::string_view { - switch (result) { - case VK_SUCCESS: - return "VK_SUCCESS"; - case VK_ERROR_LAYER_NOT_PRESENT: - return "VK_ERROR_LAYER_NOT_PRESENT"; - case VK_ERROR_EXTENSION_NOT_PRESENT: - return "VK_ERROR_EXTENSION_NOT_PRESENT"; - case VK_ERROR_INCOMPATIBLE_DRIVER: - return "VK_ERROR_INCOMPATIBLE_DRIVER"; - case VK_ERROR_OUT_OF_DATE_KHR: - return "VK_ERROR_OUT_OF_DATE_KHR"; - default: - return "VK_UNKNOWN_ERROR"; +void destroy_probe_surface(VkInstance instance, + VkSurfaceKHR &surface) noexcept { + if (instance == VK_NULL_HANDLE || surface == VK_NULL_HANDLE) { + return; } -} -void throw_if_vk_failed(VkResult result, std::string_view step) { - if (result != VK_SUCCESS) { - throw std::runtime_error( - std::format("{} failed with {}", step, vk_result_to_string(result))); - } + vkDestroySurfaceKHR(instance, surface, nullptr); + surface = VK_NULL_HANDLE; } } // namespace @@ -175,48 +184,38 @@ util::Status VulkanContext::init() { QUARK_TRY_STATUS(create_instance()); - create_surface(); QUARK_TRY_STATUS(create_device()); - create_command_pool(); - create_swapchain(); - create_swapchain_image_views(); - create_render_pass(); - create_framebuffers(); - create_command_buffers(); - create_sync_objects(); + QUARK_TRY_STATUS(create_gpu_timeline()); + QUARK_TRY_STATUS(create_retirement_queue()); - return {}; -} + QUARK_TRY_STATUS(create_presenter()); -VulkanContext::~VulkanContext() { - if (device_.valid()) { - VkDevice logical_device = device_.vk_device(); - vkDeviceWaitIdle(logical_device); - - for (auto index{0UZ}; index < kMaxFramesInFlight; ++index) { - if (image_available_semaphores_[index] != VK_NULL_HANDLE) { - vkDestroySemaphore(logical_device, image_available_semaphores_[index], - nullptr); - } - if (in_flight_fences_[index] != VK_NULL_HANDLE) { - vkDestroyFence(logical_device, in_flight_fences_[index], nullptr); - } - } + images_in_flight_.assign(presenter_.swapchain().images().size(), 0); + swapchain_images_initialized_.assign(presenter_.swapchain().images().size(), + false); - cleanup_swapchain(); + QUARK_TRY_STATUS(create_frame()); - if (command_pool_ != VK_NULL_HANDLE) { - vkDestroyCommandPool(logical_device, command_pool_, nullptr); - } + QUARK_TRY_STATUS(create_render_pass()); + QUARK_TRY_STATUS(create_framebuffers()); - device_.destroy(); - } + return {}; +} - if (surface_ != VK_NULL_HANDLE) { - vkDestroySurfaceKHR(instance_.vk_instance(), surface_, nullptr); - surface_ = VK_NULL_HANDLE; +VulkanContext::~VulkanContext() { + if (device_.validate()) { + VkDevice device = device_.vk_device(); + vkDeviceWaitIdle(device); } + frame_.destroy(); + retirement_queue_.destroy(); + gpu_timeline_.destroy(); + + cleanup_swapchain(); + presenter_.destroy(); + device_.destroy(); + instance_.destroy(); } @@ -226,14 +225,14 @@ util::Status VulkanContext::run() { while (!window_->should_close()) { window_->poll_events(); - draw_frame(); + QUARK_TRY_STATUS(draw_frame()); } - if (device_.valid()) { + if (device_.validate()) { vkDeviceWaitIdle(device_.vk_device()); } - return {}; + QUARK_OK(); } void VulkanContext::create_window() { @@ -263,139 +262,129 @@ util::Status VulkanContext::create_instance() { QUARK_ERR(util::Errc::Unsupported, "Window does not provide IVulkanSurfaceSource")); - Instance::CreateInfo ci{}; - ci.app_name = "quark-engine"; - ci.engine_name = "quark"; - ci.api_version = VK_API_VERSION_1_3; - ci.enable_debug_messenger = true; - ci.extensions = surface->required_instance_extensions(); + InstanceBundle::CreateInfo ci{}; + ci.instance.app_name = "quark-engine"; + ci.instance.engine_name = "quark"; + ci.instance.api_version = choose_instance_api_version(); + ci.instance.enable_debug_messenger = true; + ci.instance.extensions = surface->required_instance_extensions(); QUARK_TRY_STATUS(instance_.create(ci)); - return {}; -} - -void VulkanContext::create_surface() { - const auto *surface = platform::query(*window_); - if (surface == nullptr) { - throw std::runtime_error("Window does not provide IVulkanSurfaceSource"); - } - surface_ = surface->create_surface(instance_.vk_instance()); - if (surface_ == VK_NULL_HANDLE) { - throw std::runtime_error("create_surface returned VK_NULL_HANDLE"); - } + QUARK_OK(); } util::Status VulkanContext::create_device() { - Device::CreateInfo ci{}; - ci.instance = instance_.vk_instance(); - ci.surface = surface_; - ci.required_extensions = {kSwapchainExtension}; - QUARK_TRY_STATUS(device_.create(ci)); + VkSurfaceKHR surface = VK_NULL_HANDLE; + QUARK_TRY_ASSIGN(surface, + create_probe_surface(instance_.vk_instance(), *window_)); + + DeviceBundle::CreateInfo ci{}; + ci.device.instance = instance_.vk_instance(); + ci.device.surface = surface; + ci.device.required_features = static_cast( + details::DeviceFeature::TimelineSemaphore); + ci.device.preferred_features = static_cast( + details::DeviceFeature::DynamicRendering) | + static_cast( + details::DeviceFeature::Synchronization2); + ci.device.required_extensions = {kSwapchainExtension}; + + auto device_status = device_.create(ci); + destroy_probe_surface(instance_.vk_instance(), surface); + if (!device_status) { + return util::unexpected(std::move(device_status.error())); + } + + queue_submit2_ = std::bit_cast( + vkGetDeviceProcAddr(device_.vk_device(), "vkQueueSubmit2")); + cmd_begin_rendering_ = std::bit_cast( + vkGetDeviceProcAddr(device_.vk_device(), "vkCmdBeginRendering")); + cmd_end_rendering_ = std::bit_cast( + vkGetDeviceProcAddr(device_.vk_device(), "vkCmdEndRendering")); + cmd_pipeline_barrier2_ = std::bit_cast( + vkGetDeviceProcAddr(device_.vk_device(), "vkCmdPipelineBarrier2")); + + resolve_render_path(); VkPhysicalDeviceProperties properties{}; vkGetPhysicalDeviceProperties(device_.vk_physical_device(), &properties); - QUARK_LOG_INFO("Selected GPU: {}", properties.deviceName); + QUARK_LOG_INFO("Selected GPU: {} (physical device api {}.{}.{})", + properties.deviceName, + VK_VERSION_MAJOR(device_.capabilities().api_version), + VK_VERSION_MINOR(device_.capabilities().api_version), + VK_VERSION_PATCH(device_.capabilities().api_version)); - return {}; + QUARK_OK(); } -void VulkanContext::create_command_pool() { - VkCommandPoolCreateInfo pool_info{}; - pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; - pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - pool_info.queueFamilyIndex = device_.graphics_queue_family_index(); +void VulkanContext::resolve_render_path() { + const auto caps = device_.capabilities(); - throw_if_vk_failed(vkCreateCommandPool(device_.vk_device(), &pool_info, - nullptr, &command_pool_), - "vkCreateCommandPool"); -} + const bool can_use_dynamic_rendering = + api_version_at_least(caps.api_version, 1, 3) && + caps.enabled(details::DeviceFeature::DynamicRendering) && + caps.enabled(details::DeviceFeature::Synchronization2) && + queue_submit2_ != nullptr && cmd_begin_rendering_ != nullptr && + cmd_end_rendering_ != nullptr && cmd_pipeline_barrier2_ != nullptr; -void VulkanContext::create_swapchain() { - const SwapchainSupportDetails swapchain_support = - query_swapchain_support(device_.vk_physical_device(), surface_); - - const VkSurfaceFormatKHR surface_format = - choose_swapchain_surface_format(swapchain_support.formats); - const VkPresentModeKHR present_mode = - choose_swapchain_present_mode(swapchain_support.present_modes); - const VkExtent2D extent = - choose_swapchain_extent(*window_, swapchain_support.capabilities); - - uint32_t image_count = swapchain_support.capabilities.minImageCount + 1; - if (swapchain_support.capabilities.maxImageCount > 0U && - image_count > swapchain_support.capabilities.maxImageCount) { - image_count = swapchain_support.capabilities.maxImageCount; - } + render_path_ = can_use_dynamic_rendering + ? RenderPath::Vulkan13DynamicRendering + : RenderPath::Vulkan12Fallback; - VkSwapchainCreateInfoKHR create_info{}; - create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; - create_info.surface = surface_; - create_info.minImageCount = image_count; - create_info.imageFormat = surface_format.format; - create_info.imageColorSpace = surface_format.colorSpace; - create_info.imageExtent = extent; - create_info.imageArrayLayers = 1; - create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; - - const array queue_family_indices = { - device_.graphics_queue_family_index(), - device_.present_queue_family_index()}; - - if (device_.graphics_queue_family_index() != - device_.present_queue_family_index()) { - create_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT; - create_info.queueFamilyIndexCount = 2; - create_info.pQueueFamilyIndices = queue_family_indices.data(); - } else { - create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; - } + QUARK_LOG_INFO("render path selected: {}", + render_path_ == RenderPath::Vulkan13DynamicRendering + ? "vk13 dynamic rendering" + : "vk12 render pass fallback"); +} - create_info.preTransform = swapchain_support.capabilities.currentTransform; - create_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - create_info.presentMode = present_mode; - create_info.clipped = VK_TRUE; - create_info.oldSwapchain = VK_NULL_HANDLE; +util::Status VulkanContext::create_gpu_timeline() { + GpuTimeline::CreateInfo ci{}; + ci.device = device_.view(); + QUARK_TRY_STATUS(gpu_timeline_.create(ci)); + QUARK_OK(); +} - throw_if_vk_failed(vkCreateSwapchainKHR(device_.vk_device(), &create_info, - nullptr, &swapchain_), - "vkCreateSwapchainKHR"); +util::Status VulkanContext::create_presenter() { + PresenterBundle::CreateInfo ci{}; + ci.instance = instance_.vk_instance(); + ci.physical_device = device_.vk_physical_device(); + ci.device = device_.vk_device(); + ci.graphics_queue_family_index = device_.graphics_queue_family_index(); + ci.present_queue_family_index = device_.present_queue_family_index(); + ci.window = window_.get(); + + QUARK_TRY_STATUS(presenter_.create(ci)); + QUARK_OK(); +} - vkGetSwapchainImagesKHR(device_.vk_device(), swapchain_, &image_count, - nullptr); - swapchain_images_.resize(image_count); - vkGetSwapchainImagesKHR(device_.vk_device(), swapchain_, &image_count, - swapchain_images_.data()); +util::Status VulkanContext::create_frame() { + FrameBundle::CreateInfo ci{}; + ci.device = device_.view(); + ci.retire_queue = &retirement_queue_; + ci.frames_in_flight = kMaxFramesInFlight; + ci.cmd_buffer_count = + static_cast(presenter_.swapchain().images().size()); + ci.cmd_pool_flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + QUARK_TRY_STATUS(frame_.create(ci)); + QUARK_OK(); +} - swapchain_image_format_ = surface_format.format; - swapchain_extent_ = extent; +util::Status VulkanContext::create_retirement_queue() { + RetirementQueue::CreateInfo ci{}; + ci.timeline = &gpu_timeline_; + ci.reserve = 256; // TODO: tune later + QUARK_TRY_STATUS(retirement_queue_.create(ci)); + QUARK_OK(); } -void VulkanContext::create_swapchain_image_views() { - swapchain_image_views_.resize(swapchain_images_.size()); - - for (auto index{0U}; index < swapchain_images_.size(); ++index) { - VkImageViewCreateInfo create_info{}; - create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - create_info.image = swapchain_images_[index]; - create_info.viewType = VK_IMAGE_VIEW_TYPE_2D; - create_info.format = swapchain_image_format_; - create_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - create_info.subresourceRange.baseMipLevel = 0; - create_info.subresourceRange.levelCount = 1; - create_info.subresourceRange.baseArrayLayer = 0; - create_info.subresourceRange.layerCount = 1; - - throw_if_vk_failed(vkCreateImageView(device_.vk_device(), &create_info, - nullptr, - &swapchain_image_views_[index]), - "vkCreateImageView"); +util::Status VulkanContext::create_render_pass() { + if (render_path_ == RenderPath::Vulkan13DynamicRendering) { + QUARK_OK(); } -} -void VulkanContext::create_render_pass() { VkAttachmentDescription color_attachment{}; - color_attachment.format = swapchain_image_format_; + color_attachment.format = presenter_.swapchain().format(); color_attachment.samples = VK_SAMPLE_COUNT_1_BIT; color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; @@ -429,165 +418,270 @@ void VulkanContext::create_render_pass() { render_pass_info.dependencyCount = 1; render_pass_info.pDependencies = &dependency; - throw_if_vk_failed(vkCreateRenderPass(device_.vk_device(), &render_pass_info, - nullptr, &render_pass_), - "vkCreateRenderPass"); + QUARK_VK_TRY(vkCreateRenderPass(device_.vk_device(), &render_pass_info, + nullptr, &render_pass_)); + QUARK_OK(); } -void VulkanContext::create_framebuffers() { - framebuffers_.resize(swapchain_image_views_.size()); +util::Status VulkanContext::create_framebuffers() { + if (render_path_ == RenderPath::Vulkan13DynamicRendering) { + framebuffers_.clear(); + QUARK_OK(); + } + + const auto &image_views = presenter_.swapchain().image_views(); + framebuffers_.resize(image_views.size()); - for (auto index{0U}; index < swapchain_image_views_.size(); ++index) { - const array attachments = {swapchain_image_views_[index]}; + for (auto index{0U}; index < image_views.size(); ++index) { + const array attachments = {image_views[index]}; VkFramebufferCreateInfo framebuffer_info{}; framebuffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; framebuffer_info.renderPass = render_pass_; framebuffer_info.attachmentCount = 1; framebuffer_info.pAttachments = attachments.data(); - framebuffer_info.width = swapchain_extent_.width; - framebuffer_info.height = swapchain_extent_.height; + framebuffer_info.width = presenter_.swapchain().extent().width; + framebuffer_info.height = presenter_.swapchain().extent().height; framebuffer_info.layers = 1; - throw_if_vk_failed(vkCreateFramebuffer(device_.vk_device(), - &framebuffer_info, nullptr, - &framebuffers_[index]), - "vkCreateFramebuffer"); + QUARK_VK_TRY(vkCreateFramebuffer(device_.vk_device(), &framebuffer_info, + nullptr, &framebuffers_[index])); } -} - -void VulkanContext::create_command_buffers() { - command_buffers_.resize(framebuffers_.size()); - VkCommandBufferAllocateInfo alloc_info{}; - alloc_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - alloc_info.commandPool = command_pool_; - alloc_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - alloc_info.commandBufferCount = - static_cast(command_buffers_.size()); - - throw_if_vk_failed(vkAllocateCommandBuffers(device_.vk_device(), &alloc_info, - command_buffers_.data()), - "vkAllocateCommandBuffers"); - - for (auto index{0U}; index < command_buffers_.size(); ++index) { - VkCommandBufferBeginInfo begin_info{}; - begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - - throw_if_vk_failed( - vkBeginCommandBuffer(command_buffers_[index], &begin_info), - "vkBeginCommandBuffer"); + QUARK_OK(); +} +util::Status VulkanContext::record_command_buffer(uint32_t image_index) { + VkCommandBuffer cb = frame_.cmd()->cmd(image_index); + + QUARK_VK_TRY(vkResetCommandBuffer(cb, /*flags=*/0)); + + VkCommandBufferBeginInfo begin_info{}; + begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + + QUARK_VK_TRY(vkBeginCommandBuffer(cb, &begin_info)); + + VkClearValue clear_color{}; + clear_color.color = {{0.08F, 0.08F, 0.1F, 1.0F}}; + + if (render_path_ == RenderPath::Vulkan13DynamicRendering) { + auto *image = presenter_.swapchain().images()[image_index]; + const VkImageLayout old_layout = swapchain_images_initialized_[image_index] + ? VK_IMAGE_LAYOUT_PRESENT_SRC_KHR + : VK_IMAGE_LAYOUT_UNDEFINED; + + VkImageMemoryBarrier2 to_color{}; + to_color.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + to_color.srcStageMask = VK_PIPELINE_STAGE_2_NONE; + to_color.srcAccessMask = VK_ACCESS_2_NONE; + to_color.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + to_color.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; + to_color.oldLayout = old_layout; + to_color.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + to_color.image = image; + to_color.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + to_color.subresourceRange.baseMipLevel = 0; + to_color.subresourceRange.levelCount = 1; + to_color.subresourceRange.baseArrayLayer = 0; + to_color.subresourceRange.layerCount = 1; + + VkDependencyInfo to_color_dependency{}; + to_color_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + to_color_dependency.imageMemoryBarrierCount = 1; + to_color_dependency.pImageMemoryBarriers = &to_color; + cmd_pipeline_barrier2_(cb, &to_color_dependency); + + VkRenderingAttachmentInfo color_attachment{}; + color_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + color_attachment.imageView = + presenter_.swapchain().image_views()[image_index]; + color_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + color_attachment.clearValue = clear_color; + + VkRenderingInfo rendering_info{}; + rendering_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering_info.renderArea.offset = {.x = 0, .y = 0}; + rendering_info.renderArea.extent = presenter_.swapchain().extent(); + rendering_info.layerCount = 1; + rendering_info.colorAttachmentCount = 1; + rendering_info.pColorAttachments = &color_attachment; + + cmd_begin_rendering_(cb, &rendering_info); + cmd_end_rendering_(cb); + + VkImageMemoryBarrier2 to_present{}; + to_present.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + to_present.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + to_present.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; + to_present.dstStageMask = VK_PIPELINE_STAGE_2_NONE; + to_present.dstAccessMask = VK_ACCESS_2_NONE; + to_present.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + to_present.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + to_present.image = image; + to_present.subresourceRange = to_color.subresourceRange; + + VkDependencyInfo to_present_dependency{}; + to_present_dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + to_present_dependency.imageMemoryBarrierCount = 1; + to_present_dependency.pImageMemoryBarriers = &to_present; + cmd_pipeline_barrier2_(cb, &to_present_dependency); + } else { VkRenderPassBeginInfo render_pass_info{}; render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; render_pass_info.renderPass = render_pass_; - render_pass_info.framebuffer = framebuffers_[index]; + render_pass_info.framebuffer = framebuffers_[image_index]; render_pass_info.renderArea.offset = {.x = 0, .y = 0}; - render_pass_info.renderArea.extent = swapchain_extent_; - - VkClearValue clear_color{}; - clear_color.color = {{0.08F, 0.08F, 0.1F, 1.0F}}; + render_pass_info.renderArea.extent = presenter_.swapchain().extent(); render_pass_info.clearValueCount = 1; render_pass_info.pClearValues = &clear_color; - vkCmdBeginRenderPass(command_buffers_[index], &render_pass_info, - VK_SUBPASS_CONTENTS_INLINE); - vkCmdEndRenderPass(command_buffers_[index]); - - throw_if_vk_failed(vkEndCommandBuffer(command_buffers_[index]), - "vkEndCommandBuffer"); + vkCmdBeginRenderPass(cb, &render_pass_info, VK_SUBPASS_CONTENTS_INLINE); + vkCmdEndRenderPass(cb); } + + QUARK_VK_TRY(vkEndCommandBuffer(cb)); + QUARK_OK(); } -void VulkanContext::create_sync_objects() { - images_in_flight_.assign(swapchain_images_.size(), VK_NULL_HANDLE); - render_finished_semaphores_per_image_.assign(swapchain_images_.size(), - VK_NULL_HANDLE); - - VkSemaphoreCreateInfo semaphore_info{}; - semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - - VkFenceCreateInfo fence_info{}; - fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT; - - for (uint32_t index = 0; index < kMaxFramesInFlight; ++index) { - throw_if_vk_failed(vkCreateSemaphore(device_.vk_device(), &semaphore_info, - nullptr, - &image_available_semaphores_[index]), - "vkCreateSemaphore(image available)"); - throw_if_vk_failed(vkCreateFence(device_.vk_device(), &fence_info, nullptr, - &in_flight_fences_[index]), - "vkCreateFence"); - } +util::Status VulkanContext::submit_frame(VkCommandBuffer command_buffer, + VkSemaphore image_available, + VkSemaphore render_finished, + uint64_t signal_value) { + if (render_path_ == RenderPath::Vulkan13DynamicRendering) { + VkSemaphoreSubmitInfo wait_sem{}; + wait_sem.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + wait_sem.semaphore = image_available; + wait_sem.value = 0; + wait_sem.stageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + wait_sem.deviceIndex = 0; + + VkSemaphoreSubmitInfo signal_sem_bin{}; + signal_sem_bin.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + signal_sem_bin.semaphore = render_finished; + signal_sem_bin.value = 0; + signal_sem_bin.stageMask = VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT; + signal_sem_bin.deviceIndex = 0; + + VkSemaphoreSubmitInfo signal_sem_tl{}; + signal_sem_tl.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + signal_sem_tl.semaphore = gpu_timeline_.semaphore(); + signal_sem_tl.value = signal_value; + signal_sem_tl.stageMask = VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT; + signal_sem_tl.deviceIndex = 0; + + VkCommandBufferSubmitInfo cb_info{}; + cb_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + cb_info.commandBuffer = command_buffer; + cb_info.deviceMask = 0; + + const std::array signals = {signal_sem_bin, + signal_sem_tl}; + + VkSubmitInfo2 submit_info{}; + submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; + submit_info.waitSemaphoreInfoCount = 1; + submit_info.pWaitSemaphoreInfos = &wait_sem; + submit_info.commandBufferInfoCount = 1; + submit_info.pCommandBufferInfos = &cb_info; + submit_info.signalSemaphoreInfoCount = + static_cast(signals.size()); + submit_info.pSignalSemaphoreInfos = signals.data(); + + QUARK_VK_TRY(queue_submit2_(device_.graphics_queue(), /*submitCount=*/1, + &submit_info, /*fence=*/VK_NULL_HANDLE)); + QUARK_OK(); + } + + const VkPipelineStageFlags wait_stage = + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + const std::array signal_semaphores = { + render_finished, gpu_timeline_.semaphore()}; + const std::array signal_values = {0, signal_value}; + + VkTimelineSemaphoreSubmitInfo timeline_info{}; + timeline_info.sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO; + timeline_info.waitSemaphoreValueCount = 0; + timeline_info.pWaitSemaphoreValues = nullptr; + timeline_info.signalSemaphoreValueCount = + static_cast(signal_values.size()); + timeline_info.pSignalSemaphoreValues = signal_values.data(); - for (VkSemaphore &render_finished_semaphore : - render_finished_semaphores_per_image_) { - throw_if_vk_failed(vkCreateSemaphore(device_.vk_device(), &semaphore_info, - nullptr, &render_finished_semaphore), - "vkCreateSemaphore(render finished per image)"); - } + VkSubmitInfo submit_info{}; + submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit_info.pNext = &timeline_info; + submit_info.waitSemaphoreCount = 1; + submit_info.pWaitSemaphores = &image_available; + submit_info.pWaitDstStageMask = &wait_stage; + submit_info.commandBufferCount = 1; + submit_info.pCommandBuffers = &command_buffer; + submit_info.signalSemaphoreCount = + static_cast(signal_semaphores.size()); + submit_info.pSignalSemaphores = signal_semaphores.data(); + + QUARK_VK_TRY(vkQueueSubmit(device_.graphics_queue(), /*submitCount=*/1, + &submit_info, /*fence=*/VK_NULL_HANDLE)); + QUARK_OK(); } -void VulkanContext::draw_frame() { - throw_if_vk_failed(vkWaitForFences(device_.vk_device(), 1, - &in_flight_fences_[current_frame_], - VK_TRUE, UINT64_MAX), - "vkWaitForFences"); +util::Status VulkanContext::draw_frame() { + std::size_t drained = 0; + QUARK_TRY_ASSIGN(drained, retirement_queue_.drain()); + (void)drained; - uint32_t image_index = 0; - const VkResult acquire_result = - vkAcquireNextImageKHR(device_.vk_device(), swapchain_, UINT64_MAX, - image_available_semaphores_[current_frame_], - VK_NULL_HANDLE, &image_index); + auto view = frame_.view(); + // Wait until this frame slot is free + const uint64_t frame_value = view.sync->in_flight_value(current_frame_); + QUARK_TRY_STATUS(gpu_timeline_.wait(frame_value)); + + // Acquire image + uint32_t image_index = 0; + const VkResult acquire_result = vkAcquireNextImageKHR( + device_.vk_device(), presenter_.swapchain().handle(), UINT64_MAX, + view.sync->image_available(current_frame_), VK_NULL_HANDLE, &image_index); if (acquire_result == VK_ERROR_OUT_OF_DATE_KHR) { - recreate_swapchain(); - return; + QUARK_TRY_STATUS(recreate_swapchain()); + QUARK_OK(); } - if (acquire_result != VK_SUCCESS && acquire_result != VK_SUBOPTIMAL_KHR) { - throw_if_vk_failed(acquire_result, "vkAcquireNextImageKHR"); + QUARK_FAIL(::quark::vk::vk_error(acquire_result, "vkAcquireNextImageKHR")); } - if (images_in_flight_[image_index] != VK_NULL_HANDLE) { - throw_if_vk_failed(vkWaitForFences(device_.vk_device(), 1, - &images_in_flight_[image_index], VK_TRUE, - UINT64_MAX), - "vkWaitForFences(image)"); + // If this swapchain image is used by an earlier frame, wait for it + if (image_index < images_in_flight_.size()) { + const uint64_t image_value = images_in_flight_[image_index]; + QUARK_TRY_STATUS(gpu_timeline_.wait(image_value)); } - images_in_flight_[image_index] = in_flight_fences_[current_frame_]; - throw_if_vk_failed( - vkResetFences(device_.vk_device(), 1, &in_flight_fences_[current_frame_]), - "vkResetFences"); + QUARK_TRY_STATUS(record_command_buffer(image_index)); - const array wait_semaphores = { - image_available_semaphores_[current_frame_]}; - const array wait_stages = { - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; - const array signal_semaphores = { - render_finished_semaphores_per_image_[image_index]}; + const uint64_t signal_value = gpu_timeline_.next_signal_value(); - VkSubmitInfo submit_info{}; - submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submit_info.waitSemaphoreCount = 1; - submit_info.pWaitSemaphores = wait_semaphores.data(); - submit_info.pWaitDstStageMask = wait_stages.data(); - submit_info.commandBufferCount = 1; - submit_info.pCommandBuffers = &command_buffers_[image_index]; - submit_info.signalSemaphoreCount = 1; - submit_info.pSignalSemaphores = signal_semaphores.data(); + VkCommandBuffer cb = view.cmd->cmd(image_index); + QUARK_TRY_STATUS(submit_frame(cb, view.sync->image_available(current_frame_), + view.sync->render_finished(current_frame_), + signal_value)); + + // REMOVE TEST + // static uint64_t retire_test_id = 1; + // QUARK_TRY_STATUS( + // enqueue_retire_test(retirement_queue_, signal_value, + // retire_test_id++)); + + view.sync->mark_submitted(current_frame_, signal_value); + images_in_flight_[image_index] = signal_value; + swapchain_images_initialized_[image_index] = true; - throw_if_vk_failed(vkQueueSubmit(device_.graphics_queue(), 1, &submit_info, - in_flight_fences_[current_frame_]), - "vkQueueSubmit"); + const array swapchains = {presenter_.swapchain().handle()}; + const array present_wait = { + view.sync->render_finished(current_frame_)}; - const array swapchains = {swapchain_}; VkPresentInfoKHR present_info{}; present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; present_info.waitSemaphoreCount = 1; - present_info.pWaitSemaphores = signal_semaphores.data(); + present_info.pWaitSemaphores = present_wait.data(); present_info.swapchainCount = 1; present_info.pSwapchains = swapchains.data(); present_info.pImageIndices = &image_index; @@ -597,53 +691,33 @@ void VulkanContext::draw_frame() { if (present_result == VK_ERROR_OUT_OF_DATE_KHR || present_result == VK_SUBOPTIMAL_KHR) { - recreate_swapchain(); - } else { - throw_if_vk_failed(present_result, "vkQueuePresentKHR"); + QUARK_TRY_STATUS(recreate_swapchain()); + } else if (present_result != VK_SUCCESS) { + QUARK_FAIL(::quark::vk::vk_error(present_result, "vkQueuePresentKHR")); } current_frame_ = (current_frame_ + 1U) % kMaxFramesInFlight; + QUARK_OK(); } void VulkanContext::cleanup_swapchain() { - for (auto *semaphore : render_finished_semaphores_per_image_) { - if (semaphore != VK_NULL_HANDLE) { - vkDestroySemaphore(device_.vk_device(), semaphore, nullptr); + for (VkFramebuffer framebuffer : framebuffers_) { + if (framebuffer != VK_NULL_HANDLE) { + vkDestroyFramebuffer(device_.vk_device(), framebuffer, nullptr); } } - render_finished_semaphores_per_image_.clear(); - - for (auto *framebuffer : framebuffers_) { - vkDestroyFramebuffer(device_.vk_device(), framebuffer, nullptr); - } framebuffers_.clear(); - if (!command_buffers_.empty()) { - vkFreeCommandBuffers(device_.vk_device(), command_pool_, - static_cast(command_buffers_.size()), - command_buffers_.data()); - command_buffers_.clear(); - } - if (render_pass_ != VK_NULL_HANDLE) { vkDestroyRenderPass(device_.vk_device(), render_pass_, nullptr); render_pass_ = VK_NULL_HANDLE; } - for (auto *image_view : swapchain_image_views_) { - vkDestroyImageView(device_.vk_device(), image_view, nullptr); - } - swapchain_image_views_.clear(); - - if (swapchain_ != VK_NULL_HANDLE) { - vkDestroySwapchainKHR(device_.vk_device(), swapchain_, nullptr); - swapchain_ = VK_NULL_HANDLE; - } - - swapchain_images_.clear(); + presenter_.destroy_swapchain(); + swapchain_images_initialized_.clear(); } -void VulkanContext::recreate_swapchain() { +util::Status VulkanContext::recreate_swapchain() { int width{0}; int height{0}; while (width == 0 || height == 0) { @@ -654,25 +728,21 @@ void VulkanContext::recreate_swapchain() { vkDeviceWaitIdle(device_.vk_device()); cleanup_swapchain(); - create_swapchain(); - create_swapchain_image_views(); - create_render_pass(); - create_framebuffers(); - create_command_buffers(); - - images_in_flight_.assign(swapchain_images_.size(), VK_NULL_HANDLE); - render_finished_semaphores_per_image_.assign(swapchain_images_.size(), - VK_NULL_HANDLE); - - VkSemaphoreCreateInfo semaphore_info{}; - semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - for (auto index{0U}; index < render_finished_semaphores_per_image_.size(); - ++index) { - throw_if_vk_failed( - vkCreateSemaphore(device_.vk_device(), &semaphore_info, nullptr, - &render_finished_semaphores_per_image_[index]), - "vkCreateSemaphore(render finished per image recreate)"); - } + QUARK_TRY_STATUS(presenter_.recreate_swapchain()); + + images_in_flight_.assign(presenter_.swapchain().images().size(), 0); + swapchain_images_initialized_.assign(presenter_.swapchain().images().size(), + false); + + QUARK_TRY_STATUS(create_render_pass()); + QUARK_TRY_STATUS(create_framebuffers()); + + QUARK_TRY_STATUS(frame_.cmd()->resize( + static_cast(presenter_.swapchain().images().size()))); + QUARK_TRY_STATUS(frame_.sync()->resize( + static_cast(presenter_.swapchain().images().size()))); + + QUARK_OK(); } } // namespace quark::vk diff --git a/src/backend/vulkan/vulkan_context.hpp b/src/backend/vulkan/vulkan_context.hpp index a7352e5..93011bb 100644 --- a/src/backend/vulkan/vulkan_context.hpp +++ b/src/backend/vulkan/vulkan_context.hpp @@ -3,10 +3,14 @@ #include #include #include +#include #include #include #include +#include #include +#include +#include #include #include @@ -28,46 +32,59 @@ class VulkanContext final { VulkanContext &operator=(VulkanContext &&) = delete; private: + enum class RenderPath : uint8_t { + Vulkan12Fallback, + Vulkan13DynamicRendering, + }; + util::Status init(); void create_window(); + util::Status create_instance(); - void create_surface(); util::Status create_device(); - void create_swapchain(); - void create_swapchain_image_views(); - void create_render_pass(); - void create_framebuffers(); - void create_command_pool(); - void create_command_buffers(); - void create_sync_objects(); - void draw_frame(); + util::Status create_gpu_timeline(); + + util::Status create_presenter(); + void resolve_render_path(); + + util::Status create_frame(); + util::Status create_retirement_queue(); + util::Status record_command_buffer(uint32_t image_index); + + util::Status create_render_pass(); + util::Status create_framebuffers(); + util::Status submit_frame(VkCommandBuffer command_buffer, + VkSemaphore image_available, + VkSemaphore render_finished, uint64_t signal_value); + + util::Status draw_frame(); + void cleanup_swapchain(); - void recreate_swapchain(); + util::Status recreate_swapchain(); std::unique_ptr window_; InstanceBundle instance_; DeviceBundle device_; + PresenterBundle presenter_; - VkSurfaceKHR surface_{VK_NULL_HANDLE}; - VkSwapchainKHR swapchain_{VK_NULL_HANDLE}; - VkFormat swapchain_image_format_{VK_FORMAT_UNDEFINED}; - VkExtent2D swapchain_extent_{}; - vector swapchain_images_; - vector swapchain_image_views_; + static constexpr uint32_t kMaxFramesInFlight{2}; - VkRenderPass render_pass_{VK_NULL_HANDLE}; + FrameBundle frame_; + GpuTimeline gpu_timeline_; // global timeline semaphore + RetirementQueue retirement_queue_; - vector framebuffers_; - VkCommandPool command_pool_{VK_NULL_HANDLE}; - vector command_buffers_; + RenderPath render_path_{RenderPath::Vulkan12Fallback}; + PFN_vkQueueSubmit2 queue_submit2_{nullptr}; + PFN_vkCmdBeginRendering cmd_begin_rendering_{nullptr}; + PFN_vkCmdEndRendering cmd_end_rendering_{nullptr}; + PFN_vkCmdPipelineBarrier2 cmd_pipeline_barrier2_{nullptr}; - vector render_finished_semaphores_per_image_; + VkRenderPass render_pass_{VK_NULL_HANDLE}; + vector framebuffers_; - static constexpr uint32_t kMaxFramesInFlight{2}; - array image_available_semaphores_{}; - array in_flight_fences_{}; - vector images_in_flight_; + vector images_in_flight_; + vector swapchain_images_initialized_; uint32_t current_frame_{0}; }; diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt new file mode 100644 index 0000000..9899fdb --- /dev/null +++ b/src/engine/CMakeLists.txt @@ -0,0 +1,11 @@ +add_library(quark_engine INTERFACE) +add_library(quark::engine ALIAS quark_engine) + +add_subdirectory(retire) + +target_link_libraries(quark_backend_vulkan + INTERFACE + quark::engine::retire + PUBLIC + quark::headers::core +) diff --git a/src/engine/retire/CMakeLists.txt b/src/engine/retire/CMakeLists.txt new file mode 100644 index 0000000..658905f --- /dev/null +++ b/src/engine/retire/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(quark_engine_retire STATIC + retirement_queue.cpp + +) +add_library(quark::engine::retire ALIAS quark_engine_retire) + +target_link_libraries(quark_engine_retire + PUBLIC + quark::headers::core +) + +target_include_directories(quark_engine_retire + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/src/engine/retire/retirement_queue.cpp b/src/engine/retire/retirement_queue.cpp new file mode 100644 index 0000000..4d404b5 --- /dev/null +++ b/src/engine/retire/retirement_queue.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include +#include +#include + +namespace quark::vk { + +util::Status RetirementQueue::create(const CreateInfo &ci) { + destroy(); + + // TODO: replace with gpu timeline validate() + QUARK_ENSURE(ci.timeline != nullptr, + QUARK_ERR(util::Errc::InvalidArg, "timeline is null")); + + timeline_ = ci.timeline; + + if (ci.reserve > 0) { + try { + entries_.reserve(ci.reserve); + } catch (...) { + destroy(); + QUARK_FAIL(QUARK_ERR(util::Errc::OutOfMemory, "reserve failed")); + } + } + + head_ = 0; + last_enqueued_retire_at_ = 0; + QUARK_OK(); +} + +void RetirementQueue::destroy() noexcept { + for (std::size_t i = head_; i < entries_.size(); ++i) { + run_task_(entries_[i].task); + } + + entries_.clear(); + head_ = 0; + last_enqueued_retire_at_ = 0; + timeline_ = nullptr; +} + +void RetirementQueue::run_task_(Task &t) noexcept { + if (t.fn != nullptr) { + t.fn(t.ctx); + } + + if (t.cleanup != nullptr) { + t.cleanup(t.ctx); + } + + t.fn = nullptr; + t.cleanup = nullptr; + t.ctx = nullptr; +} + +util::Status RetirementQueue::enqueue(uint64_t retire_at, Task task) { + QUARK_ENSURE( + timeline_ != nullptr, + QUARK_ERR(util::Errc::InvalidState, "RetirementQueue is not created")); + QUARK_ENSURE(task.fn != nullptr, + QUARK_ERR(util::Errc::InvalidArg, "task.fn is null")); + + uint64_t completed = 0; + QUARK_TRY_ASSIGN(completed, timeline_->completed_value()); + + if (retire_at <= completed) { + run_task_(task); + QUARK_OK(); + } + + retire_at = std::max(retire_at, last_enqueued_retire_at_); + + try { + entries_.push_back(Entry{.retire_at = retire_at, .task = task}); + } catch (...) { + QUARK_FAIL(QUARK_ERR(util::Errc::OutOfMemory, "push_back failed")); + } + + last_enqueued_retire_at_ = retire_at; + QUARK_OK(); +} + +util::Result RetirementQueue::drain() noexcept { + if (timeline_ == nullptr) { + return 0; + } + + uint64_t completed = 0; + QUARK_TRY_ASSIGN(completed, timeline_->completed_value()); + + std::size_t drained = 0; + while (head_ < entries_.size()) { + Entry &e = entries_[head_]; + if (e.retire_at > completed) { + break; + } + + run_task_(e.task); + ++head_; + ++drained; + } + + compact_if_needed_(); + return drained; +} + +void RetirementQueue::compact_if_needed_() noexcept { + if (head_ == 0) { + return; + } + + const std::size_t n = entries_.size(); + const std::size_t dead = head_; + const std::size_t live = n - head_; + + constexpr std::size_t kDeadThreshold = 256; + + if (dead < kDeadThreshold && dead < live) { + return; + } + + for (std::size_t i = 0; i < live; ++i) { + entries_[i] = entries_[head_ + i]; + } + entries_.resize(live); + head_ = 0; + + if (entries_.empty()) { + last_enqueued_retire_at_ = 0; + } +} + +} // namespace quark::vk diff --git a/src/utils/diagnostics/diagnostic.cpp b/src/utils/diagnostics/diagnostic.cpp index 8aa38ce..d0bcf0c 100644 --- a/src/utils/diagnostics/diagnostic.cpp +++ b/src/utils/diagnostics/diagnostic.cpp @@ -15,14 +15,38 @@ std::shared_ptr g_sinks; void set_diagnostic_sinks(std::span sinks) noexcept { auto list = std::make_shared(sinks.begin(), sinks.end()); - std::atomic_store_explicit(&g_sinks, - std::shared_ptr(std::move(list)), - std::memory_order_release); + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4996) +#endif + + std::atomic_store_explicit( // NOLINT(deprecated-declarations) + &g_sinks, + // the suggestion to use std::atomic::> + std::shared_ptr( + std::move(list)), // This may not require a move + std::memory_order_release); + // lint from clang tidy does not work on msvc(it works on GCC, at least!), we + // should look into this more! TODO + +#ifdef _MSC_VER +#pragma warning(pop) +#endif } std::shared_ptr diagnostic_sinks_snapshot() noexcept { - return std::atomic_load_explicit(std::addressof(g_sinks), - std::memory_order_acquire); +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4996) +#endif + + return std::atomic_load_explicit( // NOLINT(deprecated-declarations) + std::addressof(g_sinks), std::memory_order_acquire); + +#ifdef _MSC_VER +#pragma warning(pop) +#endif } void report(const DiagnosticEvent &e) noexcept {