Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ CheckOptions:
value: true
- key: cppcoreguidelines-rvalue-reference-param-not-moved.IgnoreUnnamedParams
value: true
- key: readability-convert-member-functions-to-static
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely keep this true

value: false

- key: modernize-use-auto.MinTypeNameLength
value: 5
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ compile_commands.json
.cache
logs/
CMakeFiles/

latex/
# macOS
.DS_Store

Expand All @@ -13,3 +13,5 @@ vcpkg/buildtrees/
vcpkg/downloads/
vcpkg/packages/
vcpkg/installed/
.idea
\html/
221 changes: 221 additions & 0 deletions docs/rfc/RFC-0001-opaque-generational-handles.md
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>;
```
18 changes: 18 additions & 0 deletions include/quark/utils/allocator.hpp
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
2 changes: 1 addition & 1 deletion include/quark/utils/diagnostic.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ using SinkList = std::vector<DiagnosticSink>;
void set_diagnostic_sinks(std::span<const DiagnosticSink> sinks) noexcept;
std::shared_ptr<const SinkList> diagnostic_sinks_snapshot() noexcept;

void report(const DiagnosticEvent &) noexcept;
void report(const DiagnosticEvent & /*e*/) noexcept;

template <class T>
inline void report_if_error(const util::Result<T> &r) noexcept {
Expand Down
122 changes: 122 additions & 0 deletions include/quark/utils/generational_registry.hpp
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
Loading
Loading