Common: make the lock-free maps C89

AtomicHashMap.h was a pair of C++ templates. Common/AtomicMap.{h,c}
replaces it with the same structure in C89, keyed by bytes: each key's
node is pushed at its bucket's head with a CAS and stays until the map
is freed.

- atomic_slot_map: one atomic pointer slot per key, for the OGL
  pipeline programs (keyed by the PipelineProgramKey bytes) and the
  D3D state cache (keyed by each state's hex word).
- atomic_value_map: values that begin with a quiesce_node_t, published
  whole and freed once no reader is inside, for the asset library's
  id-to-files and path-to-id maps (keyed by the string's bytes).

The OGL program cache's reference count and shader id counter move to
retro_atomic too, and PipelineProgramKeyHash goes with the template
that used it.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-11-06 00:19:39 +01:00
1 parent 033f4905cc
commit d3b454d48b
10 files changed
+433 -289

No files matched your search

+1
View File
@@ -1078,6 +1078,7 @@ DOLPHIN_SOURCES_ASM :=
DOLPHIN_SOURCES_OBJCXX :=
DOLPHIN_SOURCES_C += \
$(CORE_DIR)/Source/Core/AudioCommon/FieldAudio.c \
$(CORE_DIR)/Source/Core/Common/AtomicMap.c \
$(CORE_DIR)/Source/Core/Common/CallGate.c \
$(CORE_DIR)/Source/Core/Common/FrameClock.c \
$(CORE_DIR)/Source/Core/Common/ParallelFor.c \
-226
View File
@@ -1,226 +0,0 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstddef>
#include <functional>
#include <optional>
#include <utility>
#include <retro_atomic.h>
#include "Common/Quiesce.h"
namespace Common
{
// A map any thread may read and write without a lock, for keys that are only ever added (a
// value can be replaced or removed, the key's slot stays). Each bucket is a list pushed at
// its head with a CAS; a value is a published copy, swapped whole and freed once no reader
// is inside the map's quiesce.
template <typename K, typename V, size_t BUCKETS = 4096, typename Hash = std::hash<K>>
class AtomicHashMap final
{
public:
AtomicHashMap() = default;
AtomicHashMap(const AtomicHashMap&) = delete;
AtomicHashMap& operator=(const AtomicHashMap&) = delete;
~AtomicHashMap()
{
for (auto& bucket : m_buckets)
{
auto* node = static_cast<Node*>(retro_atomic_load_acquire_ptr(&bucket));
while (node)
{
Node* const next = node->next;
delete static_cast<Value*>(retro_atomic_load_acquire_ptr(&node->value));
delete node;
node = next;
}
}
quiesce_deinit(&m_quiesce);
}
// A copy of the value for key, if it has one.
template <typename Key>
std::optional<V> Get(const Key& key) const
{
quiesce_enter(&m_quiesce);
std::optional<V> result;
if (const Node* node = Find(Bucket(key), key, nullptr))
{
if (const auto* value = static_cast<const Value*>(retro_atomic_load_acquire_ptr(&node->value)))
result = value->value;
}
quiesce_exit(&m_quiesce);
return result;
}
void Set(const K& key, V value) { Replace(key, new Value{{}, std::move(value)}); }
template <typename Key>
void Erase(const Key& key)
{
quiesce_enter(&m_quiesce);
if (Node* node = Find(Bucket(key), key, nullptr))
Retire(retro_atomic_exchange_ptr(&node->value, nullptr));
quiesce_exit(&m_quiesce);
}
private:
struct Value
{
quiesce_node_t node; // first: the free callback casts it back
V value;
};
struct Node
{
Node* next; // fixed once published
K key;
retro_atomic_ptr_t value; // Value*, or null when removed
};
template <typename Key>
retro_atomic_ptr_t* Bucket(const Key& key) const
{
return &m_buckets[Hash{}(key) % BUCKETS];
}
template <typename Key>
static Node* Find(retro_atomic_ptr_t* bucket, const Key& key, const Node* stop)
{
for (auto* node = static_cast<Node*>(retro_atomic_load_acquire_ptr(bucket)); node != stop;
node = node->next)
{
if (node->key == key)
return node;
}
return nullptr;
}
static void FreeValue(quiesce_node_t* node) { delete reinterpret_cast<Value*>(node); }
void Retire(void* value)
{
if (value)
quiesce_retire(&m_quiesce, &static_cast<Value*>(value)->node, FreeValue);
}
void Replace(const K& key, Value* value)
{
quiesce_enter(&m_quiesce);
retro_atomic_ptr_t* const bucket = Bucket(key);
Node* node = Find(bucket, key, nullptr);
if (!node)
{
auto* fresh = new Node{static_cast<Node*>(retro_atomic_load_acquire_ptr(bucket)), key, {}};
retro_atomic_ptr_init(&fresh->value, nullptr);
for (;;)
{
if (retro_atomic_cas_ptr(bucket, fresh->next, fresh))
{
node = fresh;
break;
}
// Another thread pushed meanwhile, maybe this very key.
Node* const head = static_cast<Node*>(retro_atomic_load_acquire_ptr(bucket));
if ((node = Find(bucket, key, fresh->next)) != nullptr)
{
delete fresh;
break;
}
fresh->next = head;
}
}
Retire(retro_atomic_exchange_ptr(&node->value, value));
quiesce_exit(&m_quiesce);
}
mutable retro_atomic_ptr_t m_buckets[BUCKETS]{};
mutable quiesce_t m_quiesce{};
};
// The bare form: each key gets one atomic pointer slot, inserted on first use and never
// removed, and the caller decides what the pointer means and when what it points to may go.
template <typename K, size_t BUCKETS = 1024, typename Hash = std::hash<K>>
class AtomicSlotMap final
{
public:
AtomicSlotMap() = default;
AtomicSlotMap(const AtomicSlotMap&) = delete;
AtomicSlotMap& operator=(const AtomicSlotMap&) = delete;
~AtomicSlotMap()
{
for (auto& bucket : m_buckets)
{
auto* node = static_cast<Node*>(retro_atomic_load_acquire_ptr(&bucket));
while (node)
{
Node* const next = node->next;
delete node;
node = next;
}
}
}
// The slot for key, made empty (null) the first time.
retro_atomic_ptr_t* Slot(const K& key)
{
retro_atomic_ptr_t* const bucket = &m_buckets[Hash{}(key) % BUCKETS];
auto* head = static_cast<Node*>(retro_atomic_load_acquire_ptr(bucket));
if (Node* node = Find(head, nullptr, key))
return &node->slot;
auto* fresh = new Node{head, key, {}};
retro_atomic_ptr_init(&fresh->slot, nullptr);
while (!retro_atomic_cas_ptr(bucket, fresh->next, fresh))
{
// Another thread pushed meanwhile, maybe this very key.
auto* newer = static_cast<Node*>(retro_atomic_load_acquire_ptr(bucket));
if (Node* node = Find(newer, fresh->next, key))
{
delete fresh;
return &node->slot;
}
fresh->next = newer;
}
return &fresh->slot;
}
// Calls fn(slot) for every slot; for teardown, when no other thread uses the map.
template <typename Fn>
void ForEach(Fn&& fn)
{
for (auto& bucket : m_buckets)
{
for (auto* node = static_cast<Node*>(retro_atomic_load_acquire_ptr(&bucket)); node;
node = node->next)
{
fn(&node->slot);
}
}
}
private:
struct Node
{
Node* next; // fixed once published
K key;
retro_atomic_ptr_t slot;
};
static Node* Find(Node* node, const Node* stop, const K& key)
{
for (; node != stop; node = node->next)
{
if (node->key == key)
return node;
}
return nullptr;
}
retro_atomic_ptr_t m_buckets[BUCKETS]{};
};
} // namespace Common
+229
View File
@@ -0,0 +1,229 @@
/* Copyright 2026 Dolphin Emulator Project
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "Common/AtomicMap.h"
#include <stdlib.h>
#include <string.h>
typedef struct atomic_map_node
{
struct atomic_map_node *next; /* fixed once published */
retro_atomic_ptr_t slot;
size_t len;
unsigned char key[1]; /* len bytes */
} atomic_map_node_t;
struct atomic_slot_map
{
size_t buckets;
retro_atomic_ptr_t heads[1]; /* buckets entries */
};
struct atomic_value_map
{
atomic_slot_map_t *slots;
quiesce_free_fn free_value;
quiesce_t quiesce;
};
/* FNV-1a */
static size_t atomic_map_hash(const void *key, size_t len)
{
const unsigned char *bytes = (const unsigned char*)key;
unsigned long hash = 2166136261UL;
size_t i;
for (i = 0; i < len; i++)
{
hash ^= bytes[i];
hash = (hash * 16777619UL) & 0xffffffffUL;
}
return (size_t)hash;
}
static atomic_map_node_t *atomic_map_find(atomic_map_node_t *node,
const atomic_map_node_t *stop, const void *key, size_t len)
{
for (; node != stop; node = node->next)
{
if (node->len == len && memcmp(node->key, key, len) == 0)
return node;
}
return NULL;
}
static retro_atomic_ptr_t *atomic_map_bucket(atomic_slot_map_t *map,
const void *key, size_t len)
{
return &map->heads[atomic_map_hash(key, len) % map->buckets];
}
atomic_slot_map_t *atomic_slot_map_new(size_t buckets)
{
atomic_slot_map_t *map;
size_t i;
if (buckets == 0)
buckets = 1;
map = (atomic_slot_map_t*)malloc(sizeof(*map)
+ (buckets - 1) * sizeof(map->heads[0]));
if (!map)
return NULL;
map->buckets = buckets;
for (i = 0; i < buckets; i++)
retro_atomic_ptr_init(&map->heads[i], NULL);
return map;
}
void atomic_slot_map_free(atomic_slot_map_t *map)
{
size_t i;
if (!map)
return;
for (i = 0; i < map->buckets; i++)
{
atomic_map_node_t *node =
(atomic_map_node_t*)retro_atomic_load_acquire_ptr(&map->heads[i]);
while (node)
{
atomic_map_node_t *next = node->next;
free(node);
node = next;
}
}
free(map);
}
retro_atomic_ptr_t *atomic_slot_map_find(atomic_slot_map_t *map,
const void *key, size_t len)
{
atomic_map_node_t *node = atomic_map_find(
(atomic_map_node_t*)retro_atomic_load_acquire_ptr(
atomic_map_bucket(map, key, len)), NULL, key, len);
return node ? &node->slot : NULL;
}
retro_atomic_ptr_t *atomic_slot_map_slot(atomic_slot_map_t *map,
const void *key, size_t len)
{
retro_atomic_ptr_t *bucket = atomic_map_bucket(map, key, len);
atomic_map_node_t *head =
(atomic_map_node_t*)retro_atomic_load_acquire_ptr(bucket);
atomic_map_node_t *node = atomic_map_find(head, NULL, key, len);
atomic_map_node_t *fresh;
if (node)
return &node->slot;
fresh = (atomic_map_node_t*)malloc(sizeof(*fresh) + (len ? len - 1 : 0));
if (!fresh)
return NULL;
fresh->next = head;
retro_atomic_ptr_init(&fresh->slot, NULL);
fresh->len = len;
if (len)
memcpy(fresh->key, key, len);
while (!retro_atomic_cas_ptr(bucket, fresh->next, fresh))
{
/* Another thread pushed meanwhile, maybe this very key. */
atomic_map_node_t *newer =
(atomic_map_node_t*)retro_atomic_load_acquire_ptr(bucket);
node = atomic_map_find(newer, fresh->next, key, len);
if (node)
{
free(fresh);
return &node->slot;
}
fresh->next = newer;
}
return &fresh->slot;
}
void atomic_slot_map_for_each(atomic_slot_map_t *map,
void (*fn)(retro_atomic_ptr_t *slot, void *ctx), void *ctx)
{
size_t i;
for (i = 0; i < map->buckets; i++)
{
atomic_map_node_t *node =
(atomic_map_node_t*)retro_atomic_load_acquire_ptr(&map->heads[i]);
for (; node; node = node->next)
fn(&node->slot, ctx);
}
}
atomic_value_map_t *atomic_value_map_new(size_t buckets, quiesce_free_fn free_value)
{
atomic_value_map_t *map = (atomic_value_map_t*)malloc(sizeof(*map));
if (!map)
return NULL;
map->slots = atomic_slot_map_new(buckets);
if (!map->slots)
{
free(map);
return NULL;
}
map->free_value = free_value;
quiesce_init(&map->quiesce);
return map;
}
static void atomic_value_map_free_slot(retro_atomic_ptr_t *slot, void *ctx)
{
atomic_value_map_t *map = (atomic_value_map_t*)ctx;
quiesce_node_t *value = (quiesce_node_t*)retro_atomic_load_acquire_ptr(slot);
if (value)
map->free_value(value);
}
void atomic_value_map_free(atomic_value_map_t *map)
{
if (!map)
return;
atomic_slot_map_for_each(map->slots, atomic_value_map_free_slot, map);
quiesce_deinit(&map->quiesce);
atomic_slot_map_free(map->slots);
free(map);
}
void atomic_value_map_enter(atomic_value_map_t *map)
{
quiesce_enter(&map->quiesce);
}
void atomic_value_map_exit(atomic_value_map_t *map)
{
quiesce_exit(&map->quiesce);
}
quiesce_node_t *atomic_value_map_get(atomic_value_map_t *map,
const void *key, size_t len)
{
retro_atomic_ptr_t *slot = atomic_slot_map_find(map->slots, key, len);
return slot ? (quiesce_node_t*)retro_atomic_load_acquire_ptr(slot) : NULL;
}
void atomic_value_map_set(atomic_value_map_t *map,
const void *key, size_t len, quiesce_node_t *value)
{
retro_atomic_ptr_t *slot;
quiesce_node_t *old = NULL;
quiesce_enter(&map->quiesce);
/* Erasing a key that was never added adds nothing. */
slot = value ? atomic_slot_map_slot(map->slots, key, len)
: atomic_slot_map_find(map->slots, key, len);
if (slot)
old = (quiesce_node_t*)retro_atomic_exchange_ptr(slot, value);
else if (value)
map->free_value(value);
if (old)
quiesce_retire(&map->quiesce, old, map->free_value);
quiesce_exit(&map->quiesce);
}
+63
View File
@@ -0,0 +1,63 @@
/* Copyright 2026 Dolphin Emulator Project
* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef DOLPHIN_COMMON_ATOMICMAP_H
#define DOLPHIN_COMMON_ATOMICMAP_H
#include <stddef.h>
#include <retro_atomic.h>
#include "Common/Quiesce.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Maps any thread may read and write without a lock, keyed by bytes.
* A key is only ever added: its node is pushed at the head of its
* bucket with a CAS and stays until the map is freed. */
/* The bare form: each key has one atomic pointer slot, null when made;
* the caller decides what the pointer means and when what it points
* to may go. */
typedef struct atomic_slot_map atomic_slot_map_t;
atomic_slot_map_t *atomic_slot_map_new(size_t buckets);
void atomic_slot_map_free(atomic_slot_map_t *map);
/* The key's slot, made the first time; NULL only when out of memory. */
retro_atomic_ptr_t *atomic_slot_map_slot(atomic_slot_map_t *map,
const void *key, size_t len);
/* The key's slot if it has one, NULL otherwise. */
retro_atomic_ptr_t *atomic_slot_map_find(atomic_slot_map_t *map,
const void *key, size_t len);
/* Calls fn on every slot; for teardown, while no other thread uses the map. */
void atomic_slot_map_for_each(atomic_slot_map_t *map,
void (*fn)(retro_atomic_ptr_t *slot, void *ctx), void *ctx);
/* A map of values. A value starts with its quiesce_node_t; it is
* published whole, and one replaced or erased is freed with free_value
* once no reader is inside the map. */
typedef struct atomic_value_map atomic_value_map_t;
atomic_value_map_t *atomic_value_map_new(size_t buckets, quiesce_free_fn free_value);
/* Frees the values still held. */
void atomic_value_map_free(atomic_value_map_t *map);
/* Readers bracket atomic_value_map_get and every use of what it
* returned with these. */
void atomic_value_map_enter(atomic_value_map_t *map);
void atomic_value_map_exit(atomic_value_map_t *map);
quiesce_node_t *atomic_value_map_get(atomic_value_map_t *map,
const void *key, size_t len);
/* Takes value, which may be NULL to erase. */
void atomic_value_map_set(atomic_value_map_t *map,
const void *key, size_t len, quiesce_node_t *value);
#ifdef __cplusplus
}
#endif
#endif
+19 -12
View File
@@ -357,21 +357,28 @@ static T* Publish(retro_atomic_ptr_t* slot, ComPtr<T>&& state)
return static_cast<T*>(retro_atomic_load_acquire_ptr(slot));
}
static void ReleaseSlot(retro_atomic_ptr_t* slot, void* ctx)
{
IUnknown* state = static_cast<IUnknown*>(retro_atomic_exchange_ptr(slot, nullptr));
if (state)
state->Release();
}
StateCache::~StateCache()
{
const auto release = [](retro_atomic_ptr_t* slot) {
if (auto* state = static_cast<IUnknown*>(retro_atomic_exchange_ptr(slot, nullptr)))
state->Release();
};
m_sampler.ForEach(release);
m_blend.ForEach(release);
m_raster.ForEach(release);
m_depth.ForEach(release);
atomic_slot_map_t* maps[4] = {m_sampler, m_blend, m_raster, m_depth};
int i;
for (i = 0; i < 4; i++)
{
atomic_slot_map_for_each(maps[i], ReleaseSlot, nullptr);
atomic_slot_map_free(maps[i]);
}
}
ID3D11SamplerState* StateCache::Get(SamplerState state)
{
retro_atomic_ptr_t* const slot = m_sampler.Slot(state);
const u64 key = state.Hex();
retro_atomic_ptr_t* const slot = atomic_slot_map_slot(m_sampler, &key, sizeof(key));
if (auto* cached = static_cast<ID3D11SamplerState*>(retro_atomic_load_acquire_ptr(slot)))
return cached;
@@ -421,7 +428,7 @@ ID3D11SamplerState* StateCache::Get(SamplerState state)
ID3D11BlendState* StateCache::Get(BlendingState state)
{
retro_atomic_ptr_t* const slot = m_blend.Slot(state.hex);
retro_atomic_ptr_t* const slot = atomic_slot_map_slot(m_blend, &state.hex, sizeof(state.hex));
if (auto* cached = static_cast<ID3D11BlendState*>(retro_atomic_load_acquire_ptr(slot)))
return cached;
@@ -497,7 +504,7 @@ ID3D11BlendState* StateCache::Get(BlendingState state)
ID3D11RasterizerState* StateCache::Get(RasterizationState state)
{
retro_atomic_ptr_t* const slot = m_raster.Slot(state.hex);
retro_atomic_ptr_t* const slot = atomic_slot_map_slot(m_raster, &state.hex, sizeof(state.hex));
if (auto* cached = static_cast<ID3D11RasterizerState*>(retro_atomic_load_acquire_ptr(slot)))
return cached;
@@ -517,7 +524,7 @@ ID3D11RasterizerState* StateCache::Get(RasterizationState state)
ID3D11DepthStencilState* StateCache::Get(DepthState state)
{
retro_atomic_ptr_t* const slot = m_depth.Slot(state.hex);
retro_atomic_ptr_t* const slot = atomic_slot_map_slot(m_depth, &state.hex, sizeof(state.hex));
if (auto* cached = static_cast<ID3D11DepthStencilState*>(retro_atomic_load_acquire_ptr(slot)))
return cached;
+6 -6
View File
@@ -11,7 +11,7 @@
#include <retro_atomic.h>
#include "Common/AtomicHashMap.h"
#include "Common/AtomicMap.h"
#include "Common/BitField.h"
#include "Common/CommonTypes.h"
#include "VideoBackends/D3D/D3DBase.h"
@@ -39,11 +39,11 @@ public:
private:
// Filled from the video thread and the shader compile workers alike; each slot holds one
// reference to its state object.
Common::AtomicSlotMap<u32> m_depth;
Common::AtomicSlotMap<u32> m_raster;
Common::AtomicSlotMap<u32> m_blend;
Common::AtomicSlotMap<SamplerState> m_sampler;
// reference to its state object. Keyed by the state's hex word(s).
atomic_slot_map_t* m_depth = atomic_slot_map_new(1024);
atomic_slot_map_t* m_raster = atomic_slot_map_new(1024);
atomic_slot_map_t* m_blend = atomic_slot_map_new(1024);
atomic_slot_map_t* m_sampler = atomic_slot_map_new(1024);
};
namespace D3D
@@ -4,7 +4,6 @@
#include "VideoBackends/OGL/ProgramShaderCache.h"
#include <array>
#include <atomic>
#include <fstream>
#include <memory>
#include <string>
@@ -49,10 +48,10 @@ static std::unique_ptr<StreamBuffer> s_buffer;
static int num_failures = 0;
static GLuint CurrentProgram = 0;
ProgramShaderCache::PipelineProgramMap ProgramShaderCache::s_pipeline_programs;
atomic_slot_map_t* ProgramShaderCache::s_pipeline_programs;
quiesce_t ProgramShaderCache::s_pipeline_program_quiesce;
static std::string s_glsl_header;
static std::atomic<u64> s_shader_counter{0};
static retro_atomic_64_t s_shader_counter;
static thread_local bool s_is_shared_context = false;
static std::string GetGLSLVersionString()
@@ -203,12 +202,11 @@ bool PipelineProgramKey::operator<(const PipelineProgramKey& rhs) const
std::tie(rhs.vertex_shader_id, rhs.geometry_shader_id, rhs.pixel_shader_id);
}
std::size_t PipelineProgramKeyHash::operator()(const PipelineProgramKey& key) const
static_assert(sizeof(PipelineProgramKey) == 3 * sizeof(u64), "the key's bytes are the key");
static void AssertSlotEmpty(retro_atomic_ptr_t* slot, void* ctx)
{
// We would really want std::hash_combine for this..
std::hash<u64> hasher;
return hasher(key.vertex_shader_id) + hasher(key.geometry_shader_id) +
hasher(key.pixel_shader_id);
DEBUG_ASSERT(!retro_atomic_load_acquire_ptr(slot));
}
StreamBuffer* ProgramShaderCache::GetUniformBuffer()
@@ -438,6 +436,8 @@ bool ProgramShaderCache::CheckProgramLinkResult(GLuint id, std::string_view vcod
void ProgramShaderCache::Init()
{
s_pipeline_programs = atomic_slot_map_new(1024);
// We have to get the UBO alignment here because
// if we generate a buffer that isn't aligned
// then the UBO will fail.
@@ -472,9 +472,9 @@ void ProgramShaderCache::Shutdown()
// All pipeline programs should have been released.
// Every pipeline is gone by now, and with it every program.
s_pipeline_programs.ForEach([](retro_atomic_ptr_t* slot) {
DEBUG_ASSERT(!retro_atomic_load_acquire_ptr(slot));
});
atomic_slot_map_for_each(s_pipeline_programs, AssertSlotEmpty, nullptr);
atomic_slot_map_free(s_pipeline_programs);
s_pipeline_programs = nullptr;
quiesce_deinit(&s_pipeline_program_quiesce);
}
@@ -543,7 +543,7 @@ PipelineProgram* ProgramShaderCache::GetPipelineProgram(const GLVertexFormat* ve
PipelineProgramKey key = {vertex_shader ? vertex_shader->GetID() : 0,
geometry_shader ? geometry_shader->GetID() : 0,
pixel_shader ? pixel_shader->GetID() : 0};
retro_atomic_ptr_t* const slot = s_pipeline_programs.Slot(key);
retro_atomic_ptr_t* const slot = atomic_slot_map_slot(s_pipeline_programs, &key, sizeof(key));
{
quiesce_enter(&s_pipeline_program_quiesce);
auto* existing = static_cast<PipelineProgram*>(retro_atomic_load_acquire_ptr(slot));
@@ -652,13 +652,14 @@ PipelineProgram* ProgramShaderCache::GetPipelineProgram(const GLVertexFormat* ve
bool ProgramShaderCache::TryReference(PipelineProgram* prog)
{
std::size_t count = prog->reference_count.load();
while (count != 0)
for (;;)
{
if (prog->reference_count.compare_exchange_weak(count, count + 1))
const int count = retro_atomic_load_acquire_int(&prog->reference_count);
if (count == 0)
return false;
if (retro_atomic_cas_int(&prog->reference_count, count, count + 1))
return true;
}
return false;
}
void ProgramShaderCache::FreePipelineProgram(quiesce_node_t* node)
@@ -668,13 +669,14 @@ void ProgramShaderCache::FreePipelineProgram(quiesce_node_t* node)
void ProgramShaderCache::ReleasePipelineProgram(PipelineProgram* prog)
{
if (--prog->reference_count > 0)
if (retro_atomic_fetch_sub_int(&prog->reference_count, 1) > 1)
return;
prog->shader.Destroy();
// Unless a newer program already took its slot.
retro_atomic_cas_ptr(s_pipeline_programs.Slot(prog->key), prog, nullptr);
retro_atomic_cas_ptr(atomic_slot_map_slot(s_pipeline_programs, &prog->key, sizeof(prog->key)),
prog, nullptr);
quiesce_retire(&s_pipeline_program_quiesce, &prog->node, FreePipelineProgram);
}
@@ -910,7 +912,7 @@ void ProgramShaderCache::CreateHeader()
u64 ProgramShaderCache::GenerateShaderID()
{
return s_shader_counter++;
return static_cast<u64>(retro_atomic_fetch_add_64(&s_shader_counter, 1));
}
bool SharedContextAsyncShaderCompiler::WorkerThreadInitMainThread(void** param)
@@ -3,14 +3,15 @@
#pragma once
#include <atomic>
#include <memory>
#include <string_view>
#include <unordered_map>
#include "Common/AtomicHashMap.h"
#include "Common/Quiesce.h"
#include <retro_atomic.h>
#include "Common/AtomicMap.h"
#include "Common/GL/GLExtensions/GLExtensions.h"
#include "Common/Quiesce.h"
#include "VideoCommon/AsyncShaderCompiler.h"
namespace OGL
@@ -52,17 +53,12 @@ struct PipelineProgramKey
bool operator<(const PipelineProgramKey& rhs) const;
};
struct PipelineProgramKeyHash
{
std::size_t operator()(const PipelineProgramKey& key) const;
};
struct PipelineProgram
{
quiesce_node_t node{}; // first: the free callback casts it back
PipelineProgramKey key{};
SHADER shader;
std::atomic_size_t reference_count{1};
retro_atomic_int_t reference_count{1};
bool binary_retrieved = false;
};
@@ -109,14 +105,12 @@ private:
// Programs are made on the video thread and on the compile workers' shared contexts: one
// slot per key, holding the live program. A program whose count fell to 0 is dead and may
// be replaced; it is freed once no thread is looking at it.
using PipelineProgramMap =
Common::AtomicSlotMap<PipelineProgramKey, 1024, PipelineProgramKeyHash>;
static bool TryReference(PipelineProgram* prog);
static void FreePipelineProgram(quiesce_node_t* node);
static void CreateAttributelessVAO();
static PipelineProgramMap s_pipeline_programs;
static atomic_slot_map_t* s_pipeline_programs; // keyed by the PipelineProgramKey bytes
static quiesce_t s_pipeline_program_quiesce;
static u32 s_ubo_buffer_size;
@@ -35,8 +35,43 @@ std::size_t GetAssetSize(const CustomTextureData& data)
}
return total;
}
// The maps' values: each begins with the node the map frees it through.
struct AssetMapValue
{
quiesce_node_t node;
Assets::AssetMap map;
};
struct AssetIDValue
{
quiesce_node_t node;
CustomAssetLibrary::AssetID asset_id;
};
void FreeAssetMapValue(quiesce_node_t* node)
{
delete reinterpret_cast<AssetMapValue*>(node);
}
void FreeAssetIDValue(quiesce_node_t* node)
{
delete reinterpret_cast<AssetIDValue*>(node);
}
} // namespace
DirectFilesystemAssetLibrary::DirectFilesystemAssetLibrary()
: m_asset_id_to_asset_map_path(atomic_value_map_new(4096, FreeAssetMapValue)),
m_path_to_asset_id(atomic_value_map_new(4096, FreeAssetIDValue))
{
}
DirectFilesystemAssetLibrary::~DirectFilesystemAssetLibrary()
{
atomic_value_map_free(m_path_to_asset_id);
atomic_value_map_free(m_asset_id_to_asset_map_path);
}
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const AssetID& asset_id,
CustomTextureData* data)
{
@@ -70,31 +105,67 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const Ass
void DirectFilesystemAssetLibrary::SetAssetIDMapData(const AssetID& asset_id,
VideoCommon::Assets::AssetMap asset_path_map)
{
if (const auto previous_asset_map = m_asset_id_to_asset_map_path.Get(asset_id))
AssetMapValue* value;
atomic_value_map_enter(m_asset_id_to_asset_map_path);
value = reinterpret_cast<AssetMapValue*>(
atomic_value_map_get(m_asset_id_to_asset_map_path, asset_id.data(), asset_id.size()));
if (value)
{
for (const auto& [name, path] : *previous_asset_map)
m_path_to_asset_id.Erase(PathToString(path));
for (const auto& [name, path] : value->map)
{
const std::string key = PathToString(path);
atomic_value_map_set(m_path_to_asset_id, key.data(), key.size(), nullptr);
}
}
atomic_value_map_exit(m_asset_id_to_asset_map_path);
for (const auto& [name, path] : asset_path_map)
m_path_to_asset_id.Set(PathToString(path), asset_id);
{
const std::string key = PathToString(path);
AssetIDValue* id_value = new AssetIDValue{{}, asset_id};
atomic_value_map_set(m_path_to_asset_id, key.data(), key.size(), &id_value->node);
}
m_asset_id_to_asset_map_path.Set(asset_id, std::move(asset_path_map));
value = new AssetMapValue{{}, std::move(asset_path_map)};
atomic_value_map_set(m_asset_id_to_asset_map_path, asset_id.data(), asset_id.size(),
&value->node);
}
void DirectFilesystemAssetLibrary::PathModified(std::string_view path)
{
if (const auto asset_id = m_path_to_asset_id.Get(path))
AssetID asset_id;
bool found = false;
atomic_value_map_enter(m_path_to_asset_id);
const AssetIDValue* value = reinterpret_cast<const AssetIDValue*>(
atomic_value_map_get(m_path_to_asset_id, path.data(), path.size()));
if (value)
{
asset_id = value->asset_id;
found = true;
}
atomic_value_map_exit(m_path_to_asset_id);
if (found)
{
auto& system = Core::System::GetInstance();
auto& resource_manager = system.GetCustomResourceManager();
resource_manager.MarkAssetDirty(*asset_id);
resource_manager.MarkAssetDirty(asset_id);
}
}
VideoCommon::Assets::AssetMap
DirectFilesystemAssetLibrary::GetAssetMapForID(const AssetID& asset_id) const
{
return m_asset_id_to_asset_map_path.Get(asset_id).value_or(VideoCommon::Assets::AssetMap{});
VideoCommon::Assets::AssetMap result;
atomic_value_map_enter(m_asset_id_to_asset_map_path);
const AssetMapValue* value = reinterpret_cast<const AssetMapValue*>(
atomic_value_map_get(m_asset_id_to_asset_map_path, asset_id.data(), asset_id.size()));
if (value)
result = value->map;
atomic_value_map_exit(m_asset_id_to_asset_map_path);
return result;
}
} // namespace VideoCommon
@@ -12,7 +12,7 @@
#include "VideoCommon/Assets/Types.h"
#include "VideoCommon/Assets/WatchableFilesystemAssetLibrary.h"
#include "Common/AtomicHashMap.h"
#include "Common/AtomicMap.h"
namespace VideoCommon
{
@@ -21,6 +21,11 @@ namespace VideoCommon
class DirectFilesystemAssetLibrary final : public WatchableFilesystemAssetLibrary
{
public:
DirectFilesystemAssetLibrary();
~DirectFilesystemAssetLibrary() override;
DirectFilesystemAssetLibrary(const DirectFilesystemAssetLibrary&) = delete;
DirectFilesystemAssetLibrary& operator=(const DirectFilesystemAssetLibrary&) = delete;
LoadInfo LoadTexture(const AssetID& asset_id, CustomTextureData* data) override;
// Assigns the asset id to a map of files, how this map is read is dependent on the data
@@ -35,10 +40,8 @@ private:
Assets::AssetMap GetAssetMapForID(const AssetID& asset_id) const;
// Written while mods and texture packs load, read by the loader threads and the file
// watcher: lock-free maps, each value swapped whole.
Common::AtomicHashMap<AssetID, Assets::AssetMap, 4096, std::hash<std::string_view>>
m_asset_id_to_asset_map_path;
Common::AtomicHashMap<std::string, AssetID, 4096, std::hash<std::string_view>>
m_path_to_asset_id;
// watcher: lock-free maps keyed by the string's bytes, each value swapped whole.
atomic_value_map_t* m_asset_id_to_asset_map_path; // AssetMapValue
atomic_value_map_t* m_path_to_asset_id; // AssetIDValue
};
} // namespace VideoCommon