-
Notifications
You must be signed in to change notification settings - Fork 0
docs: add RFC for opaque generational handles #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
gituser12981u2
wants to merge
6
commits into
alex_stuff
Choose a base branch
from
docs/rfc-0001-opaque-generational-handles
base: alex_stuff
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0c1387c
docs: add RFC for opaque generational handles
gituser12981u2 ae64e9c
feat: add sketch for opaque handle
alexcu2718 a2eb943
fix: CI for macos before other changes made
alexcu2718 9578225
fix: CI for macos before other changes made
alexcu2718 35abf9f
PR: fixes, need to squash commits...later
alexcu2718 85f080b
fix: clean up suggested changes
alexcu2718 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| # 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 <class H> | ||
| concept OpaqueHandle = | ||
| std::equality_comparable<H> && | ||
| requires(H handle) { | ||
| { handle.index } -> std::convertible_to<uint32_t>; | ||
| { handle.generation } -> std::convertible_to<uint32_t>; | ||
| }; | ||
| ``` | ||
|
|
||
| ### 4.2 Generic Handle Type | ||
|
|
||
| #### 4.2.1 Definition | ||
|
|
||
| The engine shall provide a generic handle template parameterized by a tag type: | ||
|
|
||
| ```cpp | ||
| template <class Tag> | ||
| struct GenericHandle final { | ||
| uint32_t index = 0; | ||
| uint32_t generation = 0; | ||
|
|
||
| friend constexpr bool operator==(GenericHandle, GenericHandle) noexcept = default; | ||
| } | ||
| ``` | ||
|
|
||
| #### 4.2.2 Requirements | ||
|
|
||
| `GenericHandle<Tag>`: | ||
|
|
||
| - 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<FrameHandleTag>; | ||
| using InstanceHandle = GenericHandle<InstanceHandleTag>; | ||
| ``` | ||
|
|
||
| ### 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: | ||
|
|
||
| - 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 | ||
|
|
||
| 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. | ||
|
|
||
| ## 6. Example | ||
|
|
||
| ```cpp | ||
| struct FrameHandleTag; | ||
|
|
||
| using FrameHandle = GenericHandle<FrameHandleTag>; | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| #pragma once | ||
|
|
||
| #include <memory_resource> | ||
|
|
||
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| #pragma once | ||
|
|
||
| #include <cstdint> | ||
| #include <memory_resource> | ||
| #include <quark/utils/allocator.hpp> | ||
| #include <quark/utils/generic_handle.hpp> | ||
| #include <quark/utils/raii.hpp> | ||
| #include <quark/utils/result.hpp> | ||
| #include <vector> | ||
|
|
||
| namespace util { | ||
|
|
||
| template <class T, OpaqueHandle Handle> 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<Handle> create(const CreateInfo &create_info) { | ||
| uint32_t index = 0; | ||
|
|
||
| if (!free_.empty()) { | ||
| index = free_.back(); | ||
| free_.pop_back(); | ||
| } else { | ||
| index = static_cast<uint32_t>(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<Slot> slots_; | ||
| std::pmr::vector<uint32_t> free_; | ||
| }; | ||
|
|
||
| } // namespace util |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely keep this true