From 0c1387ce43c804b82c413930f000c2db9b1cdcc7 Mon Sep 17 00:00:00 2001 From: gituser12981u2 Date: Sun, 22 Mar 2026 12:44:03 -0700 Subject: [PATCH 1/6] docs: add RFC for opaque generational handles --- .../RFC-0001-opaque-generational-handles.md | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 docs/rfc/RFC-0001-opaque-generational-handles.md diff --git a/docs/rfc/RFC-0001-opaque-generational-handles.md b/docs/rfc/RFC-0001-opaque-generational-handles.md new file mode 100644 index 0000000..10770cd --- /dev/null +++ b/docs/rfc/RFC-0001-opaque-generational-handles.md @@ -0,0 +1,189 @@ +# RFC: Opaque Generational Handles + +**Status:** Draft +**Author:** gituser12981u2 +**Target:** Internal Engine Architecture +**Date:** March, 2026 +**Scope**: Opaque handle types used for resource addressing +and lifetime + +## 1. Abstract + +This document defines the structure and requirements for opaque +generational handle types passed out by registry systems. These handles +provide a stable, type safe mechanism for referencing registry managed +resources. + +## 2. Motivation + +Engine systems require a mechanism to reference resources without: + +- Exposing raw backend handles +- Allowing accidental destruction or mutation +- Introducing aliasing or lifetime ambiguity + +Opaque generational handles address these concerns by: + +- Decoupling identity from storage +- Enabling validation via generation counters +- Supporting efficient slot reuse without dangling references + +A generic handle type: + +- Eliminates repetitive boilerplate across handle definitions +- Ensures uniform behavior across all handle types +- Prevents accidental mixing of handles from different domains +via type tagging + +## 3. Design Principles + +### 3.1 Opaque ownership boundary + +Handles do not grant ownership or destruction rights over the +underlying resource. + +### 3.2 Value Semantics + +Handle behave as lightweight, copyable value types. + +### 3.3 Stale reference detection + +- Handles must support detection of invalid or outdated references via +generation tracking. + +### 3.4 Minimal structure + +The handle representation is intentionally minimal to reduce overhead +and maximize performance. + +## 4. Specification + +### 4.1 OpaqueHandle + +#### 4.1.1 Definition + +A type satisfying OpaqueHandle: + +- Encodes an index into a storage domain (e.g., registry slot array) +- Encodes a generation counter associated with that index +- Supports value semantics +- Is equality comparable + +#### 4.1.2 Requirements + +An opaque handle type shall: + +- Expose an index member convertible to uint32_t +- Expose a generation member convertible to uint32_t +- Satisfy std::equality_comparable + +#### 4.1.3 Specification + +```cpp +template +concept OpaqueHandle = + std::equality_comparable && + requires(H handle) { + { handle.index } -> std::convertible_to; + { handle.generation } -> std::convertible_to; + }; +``` + +### 4.2 Generic Handle Type + +#### 4.2.1 Definition + +The engine shall provide a generic handle template parameterized by a tag type: + +```cpp +template +struct GenericHandle final { + uint32_t index = 0; + uint32_t generation = 0; + + friend constexpr bool operator==(GenericHandle, GenericHandle) noexcept = default; +} +``` + +#### 4.2.2 Requirements + +A GenericHandle: + +- Satisfies OpaqueHandle +- Is a distinct type for each unique Tag +- Has no implicit conversions between different tag instantiations + +#### 4.2.3 Tag Types + +Tag types are empty types used solely to distinguish handle domains. + +Example: + +```cpp +struct FrameHandleTag; +struct InstanceHandleTag; + +using FrameHandle = GenericHandle; +using InstanceHandle = GenericHandle; +``` + +#### 4.2.3 Rationale + +The generic handle pattern ensures: + +- Compile-time separation between resource domains +- Elimination of repetitive handle definitions +- Consistent structure and behavior across all handles + +## 5. Semantics + +### 5.1 Identity + +A handle uniquely identifies a resource within a specific storage +domain by the pair: + +```cpp +(index, generation) +``` + +Two handles are equal if and only if both components are equal. + +### 5.2 Validity + +A handle is considered valid with respect to a registry if: + +- index refers to an in-bounds slot, and +- generation matches the current generation of that slot, and +- the slot is marked live + +Validity is encoded in the handle itself and must be checked +by the owning system. + +### 5.3 Staleness + +A handle becomes stale when: + +- The referenced slot is destroyed, and +- The slot's generation is incremented + +Stale handles must not alias newly created objects occupying +the same index. + +### 5.4 Lifetime + +Handles: + +- Do not own resources +- Do not extend resource lifetime +- May outlive the underlying resource + +Using a handle after destruction is defined behavior only +insofar as the owning system detects and rejects it. + +## 6. Example + +```cpp +struct FrameHandleTag; + +using FrameHandle = GenericHandle; +``` From ae64e9c1d666839d1c3ae850891d35a5ca6db7ee Mon Sep 17 00:00:00 2001 From: Alexander Curtis Date: Tue, 14 Apr 2026 19:00:14 +0100 Subject: [PATCH 2/6] feat: add sketch for opaque handle --- .clang-tidy | 2 + .gitignore | 4 +- .../RFC-0001-opaque-generational-handles.md | 40 +++++- include/quark/utils/allocator.hpp | 18 +++ include/quark/utils/diagnostic.hpp | 2 +- include/quark/utils/generational_registry.hpp | 122 ++++++++++++++++++ include/quark/utils/generic_handle.hpp | 32 +++++ .../quark/vk/device/details/device_handle.hpp | 16 +-- .../vk/device/details/device_registry.hpp | 36 +----- include/quark/vk/device/device_bundle.hpp | 5 + .../vk/instance/details/debug_messenger.hpp | 1 - .../vk/instance/details/instance_handle.hpp | 16 +-- .../vk/instance/details/instance_registry.hpp | 38 +----- include/quark/vk/instance/instance_bundle.hpp | 3 + src/backend/vulkan/device/device.cpp | 11 +- src/backend/vulkan/device/device_bundle.cpp | 28 +++- src/backend/vulkan/device/device_registry.cpp | 96 -------------- .../vulkan/instance/debug_messenger.cpp | 3 + src/backend/vulkan/instance/instance.cpp | 13 +- .../vulkan/instance/instance_bundle.cpp | 11 +- .../vulkan/instance/instance_registry.cpp | 97 -------------- src/backend/vulkan/presentation/swapchain.cpp | 6 +- src/backend/vulkan/vulkan_context.cpp | 12 +- src/backend/vulkan/vulkan_context.hpp | 2 +- src/main.cpp | 1 + src/platform/window/glfw_window.cpp | 8 +- src/utils/diagnostics/diagnostic.cpp | 11 +- src/utils/diagnostics/sinks/default_sink.cpp | 5 +- src/utils/diagnostics/sinks/spdlog_sink.cpp | 7 +- vcpkg | 2 +- 30 files changed, 317 insertions(+), 331 deletions(-) create mode 100644 include/quark/utils/allocator.hpp create mode 100644 include/quark/utils/generational_registry.hpp create mode 100644 include/quark/utils/generic_handle.hpp diff --git a/.clang-tidy b/.clang-tidy index e50bece..4bc6fa4 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -50,6 +50,8 @@ CheckOptions: value: true - key: cppcoreguidelines-rvalue-reference-param-not-moved.IgnoreUnnamedParams value: true + - key: readability-convert-member-functions-to-static + value: false - key: modernize-use-auto.MinTypeNameLength value: 5 diff --git a/.gitignore b/.gitignore index 41153d3..4d2b7e8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ compile_commands.json .cache logs/ CMakeFiles/ - +latex/ # macOS .DS_Store @@ -13,3 +13,5 @@ vcpkg/buildtrees/ vcpkg/downloads/ vcpkg/packages/ vcpkg/installed/ +.idea +\html/ diff --git a/docs/rfc/RFC-0001-opaque-generational-handles.md b/docs/rfc/RFC-0001-opaque-generational-handles.md index 10770cd..b19dd54 100644 --- a/docs/rfc/RFC-0001-opaque-generational-handles.md +++ b/docs/rfc/RFC-0001-opaque-generational-handles.md @@ -81,7 +81,7 @@ An opaque handle type shall: ```cpp template -concept OpaqueHandle = +concept OpaqueHandle = std::equality_comparable && requires(H handle) { { handle.index } -> std::convertible_to; @@ -107,10 +107,10 @@ struct GenericHandle final { #### 4.2.2 Requirements -A GenericHandle: +`GenericHandle`: -- Satisfies OpaqueHandle -- Is a distinct type for each unique Tag +- Satisfies `OpaqueHandle` +- Is a distinct type for each unique `Tag` - Has no implicit conversions between different tag instantiations #### 4.2.3 Tag Types @@ -127,6 +127,32 @@ using FrameHandle = GenericHandle; using InstanceHandle = GenericHandle; ``` +### 4.3 Registry Storage and Allocation + +#### 4.3.1 Definition + +Registries using opaque generational handles may allocate slot storage and +free lists from a caller supplied allocator or memory resource. + +#### 4.3.2 Requirements + +A registry implementation should: + +- Default to a system allocator when no allocator is supplied +- Allow short lived backing storage such as a bump allocator for +temporary registries +- Preserve handle generation semantics regardless of allocator choice + +#### 4.3.3 Rationale + +Handle identity and stale reference detection are orthogonal to the +allocation strategy used by the registry itself. Allowing allocator +injection makes the same registry design suitable for: + +- Long lived systems using the system allocator +- Frame or scratch scoped registries backed by a bump allocator +- Tooling or tests that want explicit control over transient memory + #### 4.2.3 Rationale The generic handle pattern ensures: @@ -177,6 +203,12 @@ Handles: - Do not extend resource lifetime - May outlive the underlying resource +Registry storage may itself be temporary when backed by a short term +allocator. Destroying or resetting that storage invalidates the registry +as a whole, but does not change the semantic meaning of handles already +issued: they remain non-owning values and require the owning system for +validation. + Using a handle after destruction is defined behavior only insofar as the owning system detects and rejects it. diff --git a/include/quark/utils/allocator.hpp b/include/quark/utils/allocator.hpp new file mode 100644 index 0000000..c8b80e2 --- /dev/null +++ b/include/quark/utils/allocator.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace util { + +using BumpAllocator = std::pmr::monotonic_buffer_resource; +using MemoryResource = std::pmr::memory_resource; + +[[nodiscard]] inline MemoryResource *default_memory_resource() noexcept { + return std::pmr::get_default_resource(); +} + +[[nodiscard]] inline MemoryResource *system_memory_resource() noexcept { + return std::pmr::new_delete_resource(); +} + +} // namespace util \ No newline at end of file diff --git a/include/quark/utils/diagnostic.hpp b/include/quark/utils/diagnostic.hpp index f09dae8..d95db32 100644 --- a/include/quark/utils/diagnostic.hpp +++ b/include/quark/utils/diagnostic.hpp @@ -14,7 +14,7 @@ using SinkList = std::vector; void set_diagnostic_sinks(std::span sinks) noexcept; std::shared_ptr diagnostic_sinks_snapshot() noexcept; -void report(const DiagnosticEvent &) noexcept; +void report(const DiagnosticEvent & /*e*/) noexcept; template inline void report_if_error(const util::Result &r) noexcept { diff --git a/include/quark/utils/generational_registry.hpp b/include/quark/utils/generational_registry.hpp new file mode 100644 index 0000000..bcc6b1e --- /dev/null +++ b/include/quark/utils/generational_registry.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace util { + +template class GenerationalRegistry final { +public: + using CreateInfo = typename T::CreateInfo; + + explicit GenerationalRegistry( + std::pmr::memory_resource *memory_resource = default_memory_resource()) + : slots_(memory_resource), free_(memory_resource) {} + + ~GenerationalRegistry() = default; + + QUARK_MOVE_ONLY(GenerationalRegistry); + + void clear() noexcept { + for (auto &slot : slots_) { + if (slot.live) { + slot.object.destroy(); + slot.live = false; + ++slot.generation; + } + } + + free_.clear(); + free_.reserve(slots_.size()); + for (uint32_t index = 0; index < slots_.size(); ++index) { + free_.push_back(index); + } + } + + [[nodiscard]] util::Result create(const CreateInfo &create_info) { + uint32_t index = 0; + + if (!free_.empty()) { + index = free_.back(); + free_.pop_back(); + } else { + index = static_cast(slots_.size()); + slots_.emplace_back(); + } + + Slot &slot = slots_[index]; + + if (slot.live) { + slot.object.destroy(); + slot.live = false; + ++slot.generation; + } + + QUARK_TRY_STATUS(slot.object.create(create_info)); + slot.live = true; + + return Handle{.index = index, .generation = slot.generation}; + } + + void destroy(Handle handle) noexcept { + if (!handle.valid() || handle.index >= slots_.size()) { + return; + } + + Slot &slot = slots_[handle.index]; + if (!matches_(handle, slot)) { + return; + } + + slot.object.destroy(); + slot.live = false; + ++slot.generation; + + free_.push_back(handle.index); + } + + [[nodiscard]] bool alive(Handle handle) const noexcept { + if (!handle.valid() || handle.index >= slots_.size()) { + return false; + } + + return matches_(handle, slots_[handle.index]); + } + + T *get(Handle handle) noexcept { + if (!alive(handle)) { + return nullptr; + } + + return &slots_[handle.index].object; + } + + [[nodiscard]] const T *get(Handle handle) const noexcept { + if (!alive(handle)) { + return nullptr; + } + + return &slots_[handle.index].object; + } + +private: + struct Slot { + T object; + uint32_t generation = 1; + bool live = false; + }; + + [[nodiscard]] static bool matches_(Handle handle, const Slot &slot) noexcept { + return handle.valid() && slot.live && slot.generation == handle.generation; + } + + std::pmr::vector slots_; + std::pmr::vector free_; +}; + +} // namespace util \ No newline at end of file diff --git a/include/quark/utils/generic_handle.hpp b/include/quark/utils/generic_handle.hpp new file mode 100644 index 0000000..1b67e82 --- /dev/null +++ b/include/quark/utils/generic_handle.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +namespace util { + +template +concept OpaqueHandle = std::equality_comparable && requires(H handle) { + { handle.index } -> std::convertible_to; + { handle.generation } -> std::convertible_to; +}; + +template struct GenericHandle final { + static constexpr uint32_t invalid_index = + std::numeric_limits::max(); + + uint32_t index = invalid_index; + uint32_t generation = 0; + + [[nodiscard]] constexpr bool valid() const noexcept { + return index != invalid_index; + } + + [[nodiscard]] friend constexpr bool + operator==(GenericHandle, GenericHandle) noexcept = default; +}; + +static_assert(OpaqueHandle>); + +} // namespace util \ No newline at end of file diff --git a/include/quark/vk/device/details/device_handle.hpp b/include/quark/vk/device/details/device_handle.hpp index dedf8f9..b95c697 100644 --- a/include/quark/vk/device/details/device_handle.hpp +++ b/include/quark/vk/device/details/device_handle.hpp @@ -1,21 +1,11 @@ #pragma once -#include +#include namespace quark::vk { -struct DeviceHandle { - uint32_t index = 0xFFFF'FFFFU; - uint32_t generation = 0; +struct DeviceHandleTag; - [[nodiscard]] constexpr bool valid() const noexcept { - return index != 0xFFFF'FFFFU; - } - - [[nodiscard]] friend constexpr bool operator==(DeviceHandle a, - DeviceHandle b) noexcept { - return a.index == b.index && a.generation == b.generation; - } -}; +using DeviceHandle = util::GenericHandle; } // namespace quark::vk diff --git a/include/quark/vk/device/details/device_registry.hpp b/include/quark/vk/device/details/device_registry.hpp index 227c0fd..80ace88 100644 --- a/include/quark/vk/device/details/device_registry.hpp +++ b/include/quark/vk/device/details/device_registry.hpp @@ -3,42 +3,10 @@ #include "device.hpp" #include "device_handle.hpp" -#include -#include -#include -#include +#include namespace quark::vk { -class DeviceRegistry final { -public: - DeviceRegistry() = default; - ~DeviceRegistry() = default; - - QUARK_MOVE_ONLY(DeviceRegistry); - - void clear() noexcept; - - [[nodiscard]] util::Result create(const Device::CreateInfo &ci); - void destroy(DeviceHandle handle) noexcept; - - [[nodiscard]] bool alive(DeviceHandle handle) const noexcept; - - Device *get(DeviceHandle handle) noexcept; - [[nodiscard]] const Device *get(DeviceHandle handle) const noexcept; - -private: - struct Slot { - Device device; - uint32_t generation = 1; - bool live = false; - }; - - [[nodiscard]] static bool matches_(DeviceHandle handle, - const Slot &s) noexcept; - - std::vector slots_; - std::vector free_; -}; +using DeviceRegistry = util::GenerationalRegistry; } // namespace quark::vk diff --git a/include/quark/vk/device/device_bundle.hpp b/include/quark/vk/device/device_bundle.hpp index 2a6908b..f444dc4 100644 --- a/include/quark/vk/device/device_bundle.hpp +++ b/include/quark/vk/device/device_bundle.hpp @@ -1,6 +1,8 @@ #pragma once #include "quark/utils/result.hpp" +#include +#include #include #include #include @@ -11,7 +13,10 @@ namespace quark::vk { class DeviceBundle final { public: DeviceBundle() = default; + explicit DeviceBundle(std::pmr::memory_resource *memory_resource); explicit DeviceBundle(const Device::CreateInfo &ci); + DeviceBundle(const Device::CreateInfo &ci, + std::pmr::memory_resource *memory_resource); ~DeviceBundle() { destroy(); } QUARK_MOVE_ONLY(DeviceBundle); diff --git a/include/quark/vk/instance/details/debug_messenger.hpp b/include/quark/vk/instance/details/debug_messenger.hpp index b18196a..081c7e2 100644 --- a/include/quark/vk/instance/details/debug_messenger.hpp +++ b/include/quark/vk/instance/details/debug_messenger.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include diff --git a/include/quark/vk/instance/details/instance_handle.hpp b/include/quark/vk/instance/details/instance_handle.hpp index 0b7b930..a8a6083 100644 --- a/include/quark/vk/instance/details/instance_handle.hpp +++ b/include/quark/vk/instance/details/instance_handle.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace quark::vk { @@ -9,18 +9,8 @@ namespace quark::vk { * * Generation prevents use-after-free when slots are reused. */ -struct InstanceHandle { - uint32_t index = 0xFFFF'FFFFU; - uint32_t generation = 0; +struct InstanceHandleTag; - [[nodiscard]] constexpr bool valid() const noexcept { - return index != 0xFFFF'FFFFU; - } - - [[nodiscard]] friend constexpr bool operator==(InstanceHandle a, - InstanceHandle b) noexcept { - return a.index == b.index && a.generation == b.generation; - } -}; +using InstanceHandle = util::GenericHandle; } // namespace quark::vk diff --git a/include/quark/vk/instance/details/instance_registry.hpp b/include/quark/vk/instance/details/instance_registry.hpp index c103724..2655148 100644 --- a/include/quark/vk/instance/details/instance_registry.hpp +++ b/include/quark/vk/instance/details/instance_registry.hpp @@ -1,45 +1,11 @@ #pragma once -#include -#include -#include +#include #include #include -#include -#include namespace quark::vk { -class InstanceRegistry final { -public: - InstanceRegistry() = default; - ~InstanceRegistry() = default; - - QUARK_MOVE_ONLY(InstanceRegistry); - - void clear() noexcept; - - [[nodiscard]] util::Result - create(const Instance::CreateInfo &ci); - void destroy(InstanceHandle handle) noexcept; - - [[nodiscard]] bool alive(InstanceHandle handle) const noexcept; - - Instance *get(InstanceHandle handle) noexcept; - [[nodiscard]] const Instance *get(InstanceHandle handle) const noexcept; - -private: - struct Slot { - Instance instance; - uint32_t generation = 1; - bool live = false; - }; - - [[nodiscard]] static bool matches_(InstanceHandle handle, - const Slot &s) noexcept; - - std::vector slots_; - std::vector free_; -}; +using InstanceRegistry = util::GenerationalRegistry; } // namespace quark::vk diff --git a/include/quark/vk/instance/instance_bundle.hpp b/include/quark/vk/instance/instance_bundle.hpp index bd63359..8907f26 100644 --- a/include/quark/vk/instance/instance_bundle.hpp +++ b/include/quark/vk/instance/instance_bundle.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include @@ -13,6 +15,7 @@ namespace quark::vk { class InstanceBundle final { public: InstanceBundle() = default; + explicit InstanceBundle(std::pmr::memory_resource *memory_resource); ~InstanceBundle() { destroy(); } // TODO: assert safe move semantics in instance and debug messenger diff --git a/src/backend/vulkan/device/device.cpp b/src/backend/vulkan/device/device.cpp index 28f63af..8ccaf34 100644 --- a/src/backend/vulkan/device/device.cpp +++ b/src/backend/vulkan/device/device.cpp @@ -1,12 +1,15 @@ -#include +#include "quark/utils/diagnostic.hpp" +#include "quark/utils/error_types.hpp" +#include "quark/utils/result.hpp" +#include "quark/vk/vk_error.hpp" #include #include #include #include #include #include +#include #include -#include #include using std::vector; @@ -79,7 +82,7 @@ has_required_extensions(VkPhysicalDevice physical_device, vector props; QUARK_TRY_ASSIGN(props, enumerate_device_extensions(physical_device)); - for (const char *name : required_extensions) { + for (const char *const name : required_extensions) { QUARK_ENSURE(name != nullptr, QUARK_ERR(util::Errc::InvalidArg, "required extension name is null")); QUARK_ENSURE( @@ -240,7 +243,7 @@ build_device_extensions(VkPhysicalDevice physical_device, vector enabled = required; -#if defined(__APPLE__) +#ifdef __APPLE__ // MoltenVK usually needs portability subset; constexpr const char *kPortabilitySubset = "VK_KHR_portability_subset"; if (has_device_extension_props(props, kPortabilitySubset) && diff --git a/src/backend/vulkan/device/device_bundle.cpp b/src/backend/vulkan/device/device_bundle.cpp index 2ccd278..0a8ac0c 100644 --- a/src/backend/vulkan/device/device_bundle.cpp +++ b/src/backend/vulkan/device/device_bundle.cpp @@ -1,12 +1,28 @@ +#include +#include +#include #include #include #include #include #include #include +#include namespace quark::vk { +DeviceBundle::DeviceBundle(std::pmr::memory_resource *memory_resource) + : registry_(memory_resource) {} + +DeviceBundle::DeviceBundle(const Device::CreateInfo &ci) + : DeviceBundle(ci, util::default_memory_resource()) {} + +DeviceBundle::DeviceBundle(const Device::CreateInfo &ci, + std::pmr::memory_resource *memory_resource) + : registry_(memory_resource) { + (void)create(ci); +} + util::Status DeviceBundle::create(const Device::CreateInfo &ci) { destroy(); QUARK_TRY_ASSIGN(handle_, registry_.create(ci)); @@ -19,32 +35,32 @@ void DeviceBundle::destroy() noexcept { } VkPhysicalDevice DeviceBundle::vk_physical_device() const noexcept { - const Device *device = registry_.get(handle_); + const Device *const 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 Device *const 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 Device *const 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 Device *const 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 Device *const 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 Device *const device = registry_.get(handle_); return (device != nullptr) ? device->present_queue_family_index() : 0; } diff --git a/src/backend/vulkan/device/device_registry.cpp b/src/backend/vulkan/device/device_registry.cpp index f1cc7b2..93e8219 100644 --- a/src/backend/vulkan/device/device_registry.cpp +++ b/src/backend/vulkan/device/device_registry.cpp @@ -1,97 +1 @@ -#include -#include -#include -#include -#include #include - -namespace quark::vk { - -bool DeviceRegistry::matches_(DeviceHandle handle, const Slot &s) noexcept { - return handle.valid() && s.live && s.generation == handle.generation; -} - -void DeviceRegistry::clear() noexcept { - for (auto &s : slots_) { - if (s.live) { - s.device.destroy(); - s.live = false; - ++s.generation; - } - } - - free_.clear(); - free_.reserve(slots_.size()); - for (uint32_t i = 0; i < slots_.size(); ++i) { - free_.push_back(i); - } -} - -util::Result -DeviceRegistry::create(const Device::CreateInfo &ci) { - uint32_t idx = 0; - - if (!free_.empty()) { - idx = free_.back(); - free_.pop_back(); - } else { - idx = static_cast(slots_.size()); - slots_.push_back(Slot{}); - } - - Slot &s = slots_[idx]; - - if (s.live) { - s.device.destroy(); - s.live = false; - ++s.generation; - } - - QUARK_TRY_STATUS(s.device.create(ci)); - s.live = true; - - return DeviceHandle{.index = idx, .generation = s.generation}; -} - -void DeviceRegistry::destroy(DeviceHandle handle) noexcept { - if (!handle.valid() || handle.index >= slots_.size()) { - return; - } - - Slot &s = slots_[handle.index]; - if (!matches_(handle, s)) { - return; - } - - s.device.destroy(); - s.live = false; - ++s.generation; - - free_.push_back(handle.index); -} - -bool DeviceRegistry::alive(DeviceHandle handle) const noexcept { - if (!handle.valid() || handle.index >= slots_.size()) { - return false; - } - - return matches_(handle, slots_[handle.index]); -} - -Device *DeviceRegistry::get(DeviceHandle handle) noexcept { - if (!alive(handle)) { - return nullptr; - } - - return &slots_[handle.index].device; -} - -const Device *DeviceRegistry::get(DeviceHandle handle) const noexcept { - if (!alive(handle)) { - return nullptr; - } - - return &slots_[handle.index].device; -} - -} // namespace quark::vk diff --git a/src/backend/vulkan/instance/debug_messenger.cpp b/src/backend/vulkan/instance/debug_messenger.cpp index 23744c9..2ed525f 100644 --- a/src/backend/vulkan/instance/debug_messenger.cpp +++ b/src/backend/vulkan/instance/debug_messenger.cpp @@ -1,3 +1,6 @@ +#include "quark/utils/error_types.hpp" +#include "quark/utils/result.hpp" +#include #include #include #include diff --git a/src/backend/vulkan/instance/instance.cpp b/src/backend/vulkan/instance/instance.cpp index 1b6a45b..85921fa 100644 --- a/src/backend/vulkan/instance/instance.cpp +++ b/src/backend/vulkan/instance/instance.cpp @@ -1,3 +1,9 @@ +#include "quark/utils/details/diagnostic_details.hpp" +#include "quark/utils/diagnostic.hpp" +#include "quark/utils/error_types.hpp" +#include "quark/utils/result.hpp" +#include "quark/vk/instance/details/debug_messenger.hpp" +#include "quark/vk/vk_error.hpp" #include #include #include @@ -5,7 +11,8 @@ #include #include #include -#include +#include +#include namespace quark::vk { @@ -122,7 +129,7 @@ util::Status Instance::build_instance_extensions_( out_enable_debug_utils = false; bool enable_portability = false; -#if defined(__APPLE__) +#ifdef __APPLE__ constexpr bool portability_required = true; #else constexpr bool portability_required = false; @@ -166,7 +173,7 @@ util::Status Instance::create(const CreateInfo &ci) { VK_VERSION_PATCH(ci.api_version)); uint32_t loader_ver = VK_API_VERSION_1_0; -#if defined(VK_VERSION_1_1) +#ifdef VK_VERSION_1_1 vkEnumerateInstanceVersion(&loader_ver); #endif QUARK_LOG_INFO("loader api version: {}.{}.{}", VK_VERSION_MAJOR(loader_ver), diff --git a/src/backend/vulkan/instance/instance_bundle.cpp b/src/backend/vulkan/instance/instance_bundle.cpp index e627826..5aa3eda 100644 --- a/src/backend/vulkan/instance/instance_bundle.cpp +++ b/src/backend/vulkan/instance/instance_bundle.cpp @@ -1,13 +1,18 @@ -#include +#include "quark/utils/result.hpp" +#include #include #include #include #include #include #include +#include namespace quark::vk { +InstanceBundle::InstanceBundle(std::pmr::memory_resource *memory_resource) + : registry_(memory_resource) {} + util::Status InstanceBundle::create(const Instance::CreateInfo &ci) { destroy(); @@ -21,12 +26,12 @@ void InstanceBundle::destroy() noexcept { } VkInstance InstanceBundle::vk_instance() const noexcept { - const Instance *instance = registry_.get(handle_); + const Instance *const 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 Instance *const 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..c66196d 100644 --- a/src/backend/vulkan/instance/instance_registry.cpp +++ b/src/backend/vulkan/instance/instance_registry.cpp @@ -1,98 +1 @@ -#include -#include -#include -#include -#include #include -#include - -namespace quark::vk { - -bool InstanceRegistry::matches_(InstanceHandle handle, const Slot &s) noexcept { - return handle.valid() && s.live && s.generation == handle.generation; -} - -void InstanceRegistry::clear() noexcept { - for (auto &s : slots_) { - if (s.live) { - s.instance.destroy(); - s.live = false; - ++s.generation; - } - } - - free_.clear(); - free_.reserve(slots_.size()); - for (uint32_t i = 0; i < slots_.size(); ++i) { - free_.push_back(i); - } -} - -util::Result -InstanceRegistry::create(const Instance::CreateInfo &ci) { - uint32_t idx = 0; - - if (!free_.empty()) { - idx = free_.back(); - free_.pop_back(); - } else { - idx = static_cast(slots_.size()); - slots_.push_back(Slot{}); - } - - Slot &s = slots_[idx]; - - if (s.live) { - s.instance.destroy(); - s.live = false; - ++s.generation; - } - - QUARK_TRY_STATUS(s.instance.create(ci)); - s.live = true; - - return InstanceHandle{.index = idx, .generation = s.generation}; -} - -void InstanceRegistry::destroy(InstanceHandle handle) noexcept { - if (!handle.valid() || handle.index >= slots_.size()) { - return; - } - - Slot &s = slots_[handle.index]; - if (!matches_(handle, s)) { - return; - } - - s.instance.destroy(); - s.live = false; - ++s.generation; - - free_.push_back(handle.index); -} - -bool InstanceRegistry::alive(InstanceHandle handle) const noexcept { - if (!handle.valid() || handle.index >= slots_.size()) { - return false; - } - - return matches_(handle, slots_[handle.index]); -} - -Instance *InstanceRegistry::get(InstanceHandle handle) noexcept { - if (!alive(handle)) { - return nullptr; - } - - return &slots_[handle.index].instance; -} - -const Instance *InstanceRegistry::get(InstanceHandle handle) const noexcept { - if (!alive(handle)) { - return nullptr; - } - - return &slots_[handle.index].instance; -} - -} // namespace quark::vk diff --git a/src/backend/vulkan/presentation/swapchain.cpp b/src/backend/vulkan/presentation/swapchain.cpp index d3cab85..d295754 100644 --- a/src/backend/vulkan/presentation/swapchain.cpp +++ b/src/backend/vulkan/presentation/swapchain.cpp @@ -1,10 +1,12 @@ #include +#include +#include +#include #include #include #include #include #include - namespace quark::vk { namespace { @@ -206,7 +208,7 @@ void Swapchain::create(const CreateInfo &ci) { void Swapchain::reset() noexcept { // Destroy views if (device_ != VK_NULL_HANDLE) { - for (VkImageView v : image_views_) { + for (VkImageView const v : image_views_) { if (v != VK_NULL_HANDLE) { vkDestroyImageView(device_, v, /*pAllocator=*/nullptr); } diff --git a/src/backend/vulkan/vulkan_context.cpp b/src/backend/vulkan/vulkan_context.cpp index 98e6777..20a1f61 100644 --- a/src/backend/vulkan/vulkan_context.cpp +++ b/src/backend/vulkan/vulkan_context.cpp @@ -1,4 +1,10 @@ #include "vulkan_context.hpp" +#include "quark/platform/window/IWindow.hpp" +#include "quark/utils/diagnostic.hpp" +#include "quark/utils/error_types.hpp" +#include "quark/utils/result.hpp" +#include "quark/vk/device/details/device.hpp" +#include "quark/vk/instance/details/instance.hpp" #include #include @@ -9,13 +15,13 @@ #include #include #include -#include #include #include #include #include +#include #include -#include +#include using std::array; using std::vector; @@ -190,7 +196,7 @@ util::Status VulkanContext::init() { VulkanContext::~VulkanContext() { if (device_.valid()) { - VkDevice logical_device = device_.vk_device(); + VkDevice const logical_device = device_.vk_device(); vkDeviceWaitIdle(logical_device); for (auto index{0UZ}; index < kMaxFramesInFlight; ++index) { diff --git a/src/backend/vulkan/vulkan_context.hpp b/src/backend/vulkan/vulkan_context.hpp index a7352e5..be875e4 100644 --- a/src/backend/vulkan/vulkan_context.hpp +++ b/src/backend/vulkan/vulkan_context.hpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include using std::array; using std::vector; diff --git a/src/main.cpp b/src/main.cpp index dd3ff55..90299b9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,5 @@ #include "backend/vulkan/vulkan_context.hpp" +#include "quark/utils/error_types.hpp" #include #include diff --git a/src/platform/window/glfw_window.cpp b/src/platform/window/glfw_window.cpp index b06c3c1..f1237a0 100644 --- a/src/platform/window/glfw_window.cpp +++ b/src/platform/window/glfw_window.cpp @@ -1,6 +1,6 @@ +#include #define GLFW_INCLUDE_VULKAN #include -#include #include #include #include @@ -22,7 +22,7 @@ class GlfwVulkanSurfaceSource final : public vk::IVulkanSurfaceSource { [[nodiscard]] std::vector required_instance_extensions() const override { uint32_t count = 0; - const char **exts = glfwGetRequiredInstanceExtensions(&count); + const char **const exts = glfwGetRequiredInstanceExtensions(&count); if (exts == nullptr || count == 0) { throw std::runtime_error("glfwGetRequiredInstanceExtensions failed"); @@ -127,7 +127,7 @@ bool GlfwWindow::was_resized() const noexcept { return resized_; } void GlfwWindow::clear_resized() noexcept { resized_ = false; } void *GlfwWindow::query_interface(InterfaceId id) noexcept { - if (void *cached = iface_.find(id)) { + if (void *const cached = iface_.find(id)) { return cached; } @@ -152,7 +152,7 @@ void *GlfwWindow::query_interface(InterfaceId id) noexcept { } const void *GlfwWindow::query_interface(InterfaceId id) const noexcept { - if (const void *cached = iface_.find(id)) { + if (const void *const cached = iface_.find(id)) { return cached; } diff --git a/src/utils/diagnostics/diagnostic.cpp b/src/utils/diagnostics/diagnostic.cpp index 8aa38ce..ba71aaf 100644 --- a/src/utils/diagnostics/diagnostic.cpp +++ b/src/utils/diagnostics/diagnostic.cpp @@ -1,9 +1,10 @@ +#include "quark/utils/error_types.hpp" #include #include -#include #include #include #include +#include #include namespace util { @@ -13,18 +14,22 @@ using SinkList = std::vector; std::shared_ptr g_sinks; } // namespace +// NOLINTBEGIN 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); } + std::shared_ptr diagnostic_sinks_snapshot() noexcept { - return std::atomic_load_explicit(std::addressof(g_sinks), + + return std::atomic_load_explicit(std::addressof(g_sinks), std::memory_order_acquire); } - +// NOLINTEND void report(const DiagnosticEvent &e) noexcept { auto list = diagnostic_sinks_snapshot(); if (list && !list->empty()) { diff --git a/src/utils/diagnostics/sinks/default_sink.cpp b/src/utils/diagnostics/sinks/default_sink.cpp index 55fd14a..a58c147 100644 --- a/src/utils/diagnostics/sinks/default_sink.cpp +++ b/src/utils/diagnostics/sinks/default_sink.cpp @@ -1,5 +1,6 @@ -#include -#include +#include "quark/utils/details/diagnostic_details.hpp" +#include "quark/utils/error_types.hpp" +#include #include namespace util { diff --git a/src/utils/diagnostics/sinks/spdlog_sink.cpp b/src/utils/diagnostics/sinks/spdlog_sink.cpp index e4e54ae..ac61db4 100644 --- a/src/utils/diagnostics/sinks/spdlog_sink.cpp +++ b/src/utils/diagnostics/sinks/spdlog_sink.cpp @@ -1,4 +1,5 @@ -#include +#include "quark/utils/error_types.hpp" +#include #include #include #include @@ -106,8 +107,8 @@ void spdlog_sink(void *ctx_ptr, const util::DiagnosticEvent &e) noexcept { } const auto &w = e.where; - spdlog::source_loc loc{w.file_name(), static_cast(w.line()), - w.function_name()}; + spdlog::source_loc const loc{w.file_name(), static_cast(w.line()), + w.function_name()}; if (!e.module.empty()) { ctx->file->log(loc, to_spd(e.severity), "[{}] {}", e.module, e.msg); diff --git a/vcpkg b/vcpkg index 8eafd12..e5a1490 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit 8eafd12d19cbc42a4d5c34b5f2ce96d62e6ee9ff +Subproject commit e5a1490e1409d175932ef6014519e9ae149ddb7c From a2eb943032dfc59f969ec590ad3d8bc2eabd0fe8 Mon Sep 17 00:00:00 2001 From: Alexander Curtis Date: Sat, 18 Apr 2026 14:49:33 +0100 Subject: [PATCH 3/6] fix: CI for macos before other changes made --- src/backend/vulkan/device/device.cpp | 4 +++- src/backend/vulkan/presentation/swapchain.cpp | 9 ++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/backend/vulkan/device/device.cpp b/src/backend/vulkan/device/device.cpp index 8ccaf34..f590ba4 100644 --- a/src/backend/vulkan/device/device.cpp +++ b/src/backend/vulkan/device/device.cpp @@ -244,10 +244,12 @@ build_device_extensions(VkPhysicalDevice physical_device, vector enabled = required; #ifdef __APPLE__ +#include // 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)) { + std::find(enabled.begin(), enabled.end(), kPortabilitySubset) == + enabled.end()) { enabled.push_back(kPortabilitySubset); QUARK_LOG_INFO("portability subset: enabled"); } diff --git a/src/backend/vulkan/presentation/swapchain.cpp b/src/backend/vulkan/presentation/swapchain.cpp index d295754..5fc7c91 100644 --- a/src/backend/vulkan/presentation/swapchain.cpp +++ b/src/backend/vulkan/presentation/swapchain.cpp @@ -1,7 +1,7 @@ #include #include -#include #include +#include #include #include #include @@ -95,11 +95,6 @@ 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 @@ -208,7 +203,7 @@ void Swapchain::create(const CreateInfo &ci) { void Swapchain::reset() noexcept { // Destroy views if (device_ != VK_NULL_HANDLE) { - for (VkImageView const v : image_views_) { + for (VkImageView v : image_views_) { if (v != VK_NULL_HANDLE) { vkDestroyImageView(device_, v, /*pAllocator=*/nullptr); } From 95782251d8a94f76f5bd8a9ca8bda1a196f6f74f Mon Sep 17 00:00:00 2001 From: Alexander Curtis Date: Sat, 18 Apr 2026 14:54:23 +0100 Subject: [PATCH 4/6] fix: CI for macos before other changes made --- src/backend/vulkan/device/device.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/vulkan/device/device.cpp b/src/backend/vulkan/device/device.cpp index f590ba4..26174ae 100644 --- a/src/backend/vulkan/device/device.cpp +++ b/src/backend/vulkan/device/device.cpp @@ -11,6 +11,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif using std::vector; @@ -244,7 +247,6 @@ build_device_extensions(VkPhysicalDevice physical_device, vector enabled = required; #ifdef __APPLE__ -#include // MoltenVK usually needs portability subset; constexpr const char *kPortabilitySubset = "VK_KHR_portability_subset"; if (has_device_extension_props(props, kPortabilitySubset) && From 35abf9f7c4563a9342ec3b6b81a832449f459003 Mon Sep 17 00:00:00 2001 From: Alexander Curtis Date: Sat, 18 Apr 2026 15:31:51 +0100 Subject: [PATCH 5/6] PR: fixes, need to squash commits...later --- src/backend/vulkan/instance/debug_messenger.cpp | 1 - src/backend/vulkan/instance/instance.cpp | 2 +- src/backend/vulkan/instance/instance_bundle.cpp | 3 +-- src/backend/vulkan/vulkan_context.cpp | 9 +++------ 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/backend/vulkan/instance/debug_messenger.cpp b/src/backend/vulkan/instance/debug_messenger.cpp index 2ed525f..41b8b63 100644 --- a/src/backend/vulkan/instance/debug_messenger.cpp +++ b/src/backend/vulkan/instance/debug_messenger.cpp @@ -1,4 +1,3 @@ -#include "quark/utils/error_types.hpp" #include "quark/utils/result.hpp" #include #include diff --git a/src/backend/vulkan/instance/instance.cpp b/src/backend/vulkan/instance/instance.cpp index 85921fa..4408a53 100644 --- a/src/backend/vulkan/instance/instance.cpp +++ b/src/backend/vulkan/instance/instance.cpp @@ -1,4 +1,4 @@ -#include "quark/utils/details/diagnostic_details.hpp" + #include "quark/utils/diagnostic.hpp" #include "quark/utils/error_types.hpp" #include "quark/utils/result.hpp" diff --git a/src/backend/vulkan/instance/instance_bundle.cpp b/src/backend/vulkan/instance/instance_bundle.cpp index 5aa3eda..79dcff8 100644 --- a/src/backend/vulkan/instance/instance_bundle.cpp +++ b/src/backend/vulkan/instance/instance_bundle.cpp @@ -1,4 +1,4 @@ -#include "quark/utils/result.hpp" + #include #include #include @@ -6,7 +6,6 @@ #include #include #include -#include namespace quark::vk { diff --git a/src/backend/vulkan/vulkan_context.cpp b/src/backend/vulkan/vulkan_context.cpp index 20a1f61..e26fb2f 100644 --- a/src/backend/vulkan/vulkan_context.cpp +++ b/src/backend/vulkan/vulkan_context.cpp @@ -5,7 +5,6 @@ #include "quark/utils/result.hpp" #include "quark/vk/device/details/device.hpp" #include "quark/vk/instance/details/instance.hpp" - #include #include #include @@ -19,7 +18,6 @@ #include #include #include -#include #include #include @@ -196,7 +194,7 @@ util::Status VulkanContext::init() { VulkanContext::~VulkanContext() { if (device_.valid()) { - VkDevice const logical_device = device_.vk_device(); + VkDevice logical_device = device_.vk_device(); vkDeviceWaitIdle(logical_device); for (auto index{0UZ}; index < kMaxFramesInFlight; ++index) { @@ -243,7 +241,7 @@ util::Status VulkanContext::run() { } void VulkanContext::create_window() { - auto window = std::make_unique(); + window_ = std::make_unique(); platform::IWindow::CreateInfo ci{}; ci.width = kWindowWidth; @@ -251,8 +249,7 @@ void VulkanContext::create_window() { ci.title = kWindowTitle.data(); ci.resizable = true; - window->create(ci); - window_ = std::move(window); + window_->create(ci); } util::Status VulkanContext::create_instance() { From 85f080bd54cd555be536a8d889a16e0226ee4e0b Mon Sep 17 00:00:00 2001 From: Alexander Curtis Date: Mon, 20 Apr 2026 19:57:52 +0100 Subject: [PATCH 6/6] fix: clean up suggested changes --- src/backend/vulkan/device/device.cpp | 2 +- src/backend/vulkan/instance/debug_messenger.cpp | 2 -- src/backend/vulkan/instance/instance.cpp | 3 --- src/backend/vulkan/presentation/swapchain.cpp | 1 - 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/backend/vulkan/device/device.cpp b/src/backend/vulkan/device/device.cpp index 26174ae..a331692 100644 --- a/src/backend/vulkan/device/device.cpp +++ b/src/backend/vulkan/device/device.cpp @@ -12,7 +12,7 @@ #include #include #ifdef __APPLE__ -#include +#include // for std::find, c++23 'std::ranges::contains' is not in apple clang, irritating! #endif using std::vector; diff --git a/src/backend/vulkan/instance/debug_messenger.cpp b/src/backend/vulkan/instance/debug_messenger.cpp index 41b8b63..d22d2cb 100644 --- a/src/backend/vulkan/instance/debug_messenger.cpp +++ b/src/backend/vulkan/instance/debug_messenger.cpp @@ -1,6 +1,4 @@ -#include "quark/utils/result.hpp" #include -#include #include #include #include diff --git a/src/backend/vulkan/instance/instance.cpp b/src/backend/vulkan/instance/instance.cpp index 4408a53..c19f66b 100644 --- a/src/backend/vulkan/instance/instance.cpp +++ b/src/backend/vulkan/instance/instance.cpp @@ -1,7 +1,4 @@ -#include "quark/utils/diagnostic.hpp" -#include "quark/utils/error_types.hpp" -#include "quark/utils/result.hpp" #include "quark/vk/instance/details/debug_messenger.hpp" #include "quark/vk/vk_error.hpp" #include diff --git a/src/backend/vulkan/presentation/swapchain.cpp b/src/backend/vulkan/presentation/swapchain.cpp index 5fc7c91..e27ee05 100644 --- a/src/backend/vulkan/presentation/swapchain.cpp +++ b/src/backend/vulkan/presentation/swapchain.cpp @@ -95,7 +95,6 @@ VkExtent2D choose_extent(const platform::IWindow &window, return actual_extent; } - } // namespace void Swapchain::create(const CreateInfo &ci) {