common: SlotVector without generations

This commit is contained in:
nmzik
2026-10-24 03:48:46 +02:00
parent d7856753e0
commit 8dc0d56b7e
7 changed files with 203 additions and 126 deletions
+52 -52
View File
@@ -1,15 +1,15 @@
#ifndef EMULATOR_SRC_COMMON_SLOTVECTOR_H_
#define EMULATOR_SRC_COMMON_SLOTVECTOR_H_
#include "common/assert.h"
#include "common/abi.h"
#include "common/common.h"
#include <compare>
#include <cstdint>
#include <deque>
#include <functional>
#include <limits>
#include <optional>
#include <memory>
#include <new>
#include <numeric>
#include <utility>
#include <vector>
@@ -19,105 +19,105 @@ struct SlotId {
static constexpr uint32_t INVALID_INDEX = std::numeric_limits<uint32_t>::max();
constexpr SlotId() noexcept = default;
constexpr SlotId(uint32_t value) noexcept: index(value), generation(1) {}
constexpr SlotId(uint32_t value, uint32_t slot_generation) noexcept
: index(value), generation(slot_generation) {}
constexpr SlotId(uint32_t index) noexcept: index(index) {}
[[nodiscard]] constexpr explicit operator bool() const noexcept { return index != INVALID_INDEX; }
[[nodiscard]] constexpr explicit operator bool() const noexcept {
return index != INVALID_INDEX;
}
constexpr auto operator<=>(const SlotId&) const noexcept = default;
uint32_t index = INVALID_INDEX;
uint32_t generation = 0;
};
// Stable-address slot storage for cache resources that are intentionally non-movable.
// Fixed-size chunks keep the stored, non-movable resources in place. Occupancy is a byte per
// slot, not a bitset: a deferred erase runs while fault threads read other slots.
template <typename T>
class SlotVector {
static constexpr uint32_t ChunkSize = 256;
public:
SlotVector() = default;
KYTY_CLASS_NO_COPY(SlotVector);
~SlotVector() noexcept {
ForEach([](SlotId, T& value) { value.~T(); });
}
[[nodiscard]] T& operator[](SlotId id) noexcept {
EXIT_IF(!is_allocated(id));
return *m_values[id.index].value;
return m_chunks[id.index / ChunkSize][id.index % ChunkSize].object;
}
[[nodiscard]] const T& operator[](SlotId id) const noexcept {
EXIT_IF(!is_allocated(id));
return *m_values[id.index].value;
return m_chunks[id.index / ChunkSize][id.index % ChunkSize].object;
}
[[nodiscard]] T* try_get(SlotId id) noexcept {
return is_allocated(id) ? &*m_values[id.index].value : nullptr;
return is_allocated(id) ? &(*this)[id] : nullptr;
}
[[nodiscard]] const T* try_get(SlotId id) const noexcept {
return is_allocated(id) ? &*m_values[id.index].value : nullptr;
return is_allocated(id) ? &(*this)[id] : nullptr;
}
[[nodiscard]] bool is_allocated(SlotId id) const noexcept {
return id && id.index < m_values.size() &&
m_values[id.index].generation == id.generation && m_values[id.index].value.has_value();
return id.index < m_stored.size() && m_stored[id.index] != 0;
}
template <typename... Args>
[[nodiscard]] SlotId insert(Args&&... args) {
uint32_t index = 0;
if (m_free_list.empty()) {
index = static_cast<uint32_t>(m_values.size());
m_values.emplace_back();
m_values.back().value.emplace(std::forward<Args>(args)...);
} else {
index = m_free_list.back();
m_free_list.pop_back();
EXIT_IF(m_values[index].value.has_value());
m_values[index].value.emplace(std::forward<Args>(args)...);
Reserve();
}
++m_size;
return SlotId {index, m_values[index].generation};
const uint32_t index = m_free_list.back();
m_free_list.pop_back();
new (&(*this)[index]) T(std::forward<Args>(args)...);
m_stored[index] = 1;
return SlotId {index};
}
void erase(SlotId id) noexcept {
EXIT_IF(!is_allocated(id));
auto& slot = m_values[id.index];
slot.value.reset();
if (++slot.generation == 0) {
slot.generation = 1;
}
(*this)[id].~T();
m_stored[id.index] = 0;
m_free_list.push_back(id.index);
--m_size;
}
[[nodiscard]] size_t size() const noexcept { return m_size; }
[[nodiscard]] size_t capacity() const noexcept { return m_values.size(); }
template <typename F>
void ForEach(F&& fn) {
for (uint32_t index = 0; index < m_values.size(); ++index) {
if (m_values[index].value) {
fn(SlotId {index, m_values[index].generation}, *m_values[index].value);
for (uint32_t index = 0; index < m_stored.size(); ++index) {
if (m_stored[index] != 0) {
fn(SlotId {index}, (*this)[index]);
}
}
}
template <typename F>
void ForEach(F&& fn) const {
for (uint32_t index = 0; index < m_values.size(); ++index) {
if (m_values[index].value) {
fn(SlotId {index, m_values[index].generation}, *m_values[index].value);
for (uint32_t index = 0; index < m_stored.size(); ++index) {
if (m_stored[index] != 0) {
fn(SlotId {index}, (*this)[index]);
}
}
}
private:
struct Slot {
std::optional<T> value;
uint32_t generation = 1;
union Slot {
Slot() noexcept {}
~Slot() noexcept {}
T object;
};
std::deque<Slot> m_values;
std::vector<uint32_t> m_free_list;
size_t m_size = 0;
void Reserve() {
const auto first = static_cast<uint32_t>(m_stored.size());
m_chunks.push_back(std::make_unique<Slot[]>(ChunkSize));
m_stored.resize(first + ChunkSize);
m_free_list.resize(ChunkSize);
std::iota(m_free_list.rbegin(), m_free_list.rend(), first);
}
std::vector<std::unique_ptr<Slot[]>> m_chunks;
std::vector<uint8_t> m_stored;
std::vector<uint32_t> m_free_list;
};
} // namespace Common
@@ -125,7 +125,7 @@ private:
template <>
struct std::hash<Common::SlotId> {
[[nodiscard]] size_t operator()(Common::SlotId id) const noexcept {
return std::hash<uint64_t> {}((static_cast<uint64_t>(id.generation) << 32u) | id.index);
return std::hash<uint32_t> {}(id.index);
}
};
@@ -139,7 +139,10 @@ public:
}
static inline std::atomic<uint64_t> heap_epoch {0};
static inline std::atomic<uint64_t> serials {0};
// Unique per image, unlike ImageId: freed slots are reused.
const uint64_t serial = serials.fetch_add(1u, std::memory_order_relaxed) + 1u;
ImageInfo info;
VulkanImage backing;
std::vector<CachedImageView> views;
@@ -350,12 +350,17 @@ void ImageHeap::Release(View& view, uint32_t entry) {
}
void ImageHeap::Unreference(Element& element) {
if (auto* image = m_context.GetTextureCache().m_slot_images.try_get(element.binding.image_id)) {
if (auto* image = ImageOf(element)) {
image->heap_refs.fetch_sub(1u, std::memory_order_relaxed);
image->tick_accessed_last = m_context.GetCommandScheduler().CurrentTick();
}
}
Image* ImageHeap::ImageOf(const Element& element) const {
auto* image = m_context.GetTextureCache().m_slot_images.try_get(element.binding.image_id);
return image != nullptr && image->serial == element.serial ? image : nullptr;
}
uint32_t ImageHeap::Allocate() {
auto& scheduler = m_context.GetCommandScheduler();
while (!m_retired.empty() && scheduler.IsFree(m_retired.front().first)) {
@@ -393,7 +398,7 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
for (const auto& [value, index]: view->elements) {
const auto& binding = m_elements[index].binding;
if (!binding.image_id) continue;
const auto* image = textures.m_slot_images.try_get(binding.image_id);
const auto* image = ImageOf(m_elements[index]);
if (image == nullptr || !image->registered || image->depth_id ||
image->binding.needs_rebind) {
mark(index);
@@ -434,6 +439,7 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
if (binding.image_id) {
auto& image = textures.GetImage(binding.image_id);
image.heap_refs.fetch_add(1u, std::memory_order_relaxed);
m_elements[index].serial = image.serial;
binding.layout = image.ReadLayout();
binding.image_view = textures.FindTexture(binding.image_id, binding.desc);
if (binding.desc.info.metadata.kind == ImageMetadataKind::Dcc) {
@@ -471,8 +477,7 @@ void ImageHeap::WriteDescriptors() {
void ImageHeap::Commit(CommandBuffer& buffer, vk::PipelineBindPoint bind_point,
vk::PipelineLayout layout) {
KYTY_PROFILER_FUNCTION();
auto& textures = m_context.GetTextureCache();
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
for (auto* view: m_active) {
if (view->dirty_begin != view->dirty_end) {
const auto slots = std::span(view->slots).subspan(view->dirty_begin,
@@ -493,7 +498,7 @@ void ImageHeap::Commit(CommandBuffer& buffer, vk::PipelineBindPoint bind_point,
// this draw's discovery replaced lives until the tick ends, and its slots still select it.
std::erase_if(view->away, [&](uint32_t index) {
const auto& element = m_elements[index];
auto* image = textures.m_slot_images.try_get(element.binding.image_id);
auto* image = ImageOf(element);
if (element.refs == 0u || image == nullptr) return true;
if (image->binding.is_target || image->binding.shader_write ||
image->binding.force_general) {
@@ -57,9 +57,10 @@ private:
struct Element {
DescriptorValue value;
TextureBinding binding;
uint32_t kind = 0;
uint32_t refs = 0; // Slots of the owning view that select it.
bool stale = false;
uint32_t kind = 0;
uint32_t refs = 0; // Slots of the owning view that select it.
bool stale = false;
uint64_t serial = 0; // Image::serial of binding.image_id.
};
struct Memory;
@@ -109,6 +110,7 @@ private:
uint32_t Intern(View& view, uint32_t kind, const DescriptorValue& value);
void Release(View& view, uint32_t entry);
void Unreference(Element& element);
Image* ImageOf(const Element& element) const;
uint32_t Allocate();
void Retire(View& view);
void WriteDescriptors();