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
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ compile_commands.json
.cache
logs/
CMakeFiles/

latex
html
# macOS
.DS_Store

.idea
# i was trying jet brains lol
Comment thread
gituser12981u2 marked this conversation as resolved.
# vcpkg
vcpkg/buildtrees/
vcpkg/downloads/
Expand Down
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
124 changes: 124 additions & 0 deletions docs/rfc/RFC-0002.md
Original file line number Diff line number Diff line change
@@ -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 <class T>
concept MoveOnlyNoexcept =
std::moveable<T> &&
!std::copy_constructible<T> &&
!std::copy_assignable_v<T> &&
std::is_nothrow_move_constructible_v<T> &&
std::is_nothrow_move_assignable_v<T>;
```

### 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 <class S>
concept RegistrySlot =
requires(S slot) {
{ slot.live } -> std::convertible_to<bool>;
{ slot.generation } -> std::convertible_to<uint32_t>;
};
```

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
122 changes: 122 additions & 0 deletions include/quark/engine/retire/retirement_queue.hpp
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <cstdint>
#include <quark/utils/raii.hpp>
#include <quark/utils/result.hpp>
#include <vector>

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<std::size_t> 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<Entry> entries_;
std::size_t head_ = 0;
uint64_t last_enqueued_retire_at_ = 0;
};

} // namespace quark::vk
Loading
Loading