fix(shader): lock recompiler shared mutexes once when created

winpthreads initializes a statically initialized rwlock on its first
lock and fails a concurrent first lock with EINVAL, which
std::shared_mutex ignores, so two threads can hold the lock at once.
The source, binding plan and specialized module caches take their
first locks from several threads, so their mutexes now take and
release the lock in their constructor, as guest rwlocks do.

The GuestWriteWatch mutex the technical debt entry also named is in
the Linux-only watch, so it is not affected.
This commit is contained in:
Ahir 2026-10-11 19:22:13 +05:30
parent a11c8f7f70
commit 48dcaace78
2 changed files with 13 additions and 5 deletions

View File

@ -290,6 +290,14 @@ struct SourceEntry {
namespace {
class PrimedSharedMutex : public std::shared_mutex {
public:
PrimedSharedMutex() {
lock();
unlock();
}
};
struct ResourceProgram {
explicit ResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr) : program(std::make_unique<IrProgram>(PrepareResourceProgram(request, preparation))), plan(std::make_shared<const IrResourcePlan>(ResourceMaterializer{}.ExtractPlan(*program))) {}
@ -318,7 +326,7 @@ bool FailureMemo() {
}
std::shared_ptr<SourceEntry> getSource(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr) {
static std::shared_mutex mutex;
static PrimedSharedMutex mutex;
// Entries whose code hashes alike share a bucket; the code comparison picks the right one.
static std::unordered_map<std::vector<std::uint64_t>, std::vector<std::shared_ptr<SourceEntry>>, SourceKeyHash> sources;
struct SourceKeyStorage {};
@ -587,7 +595,7 @@ struct PreparedBindingPlan {
std::once_flag ready;
std::exception_ptr failure;
DescriptorBindingPlan bindings;
std::shared_mutex mutex;
PrimedSharedMutex mutex;
std::map<std::vector<std::uint32_t>, std::shared_ptr<PreparedModuleEntry>> modules;
};
@ -618,7 +626,7 @@ std::shared_ptr<PreparedBindingPlan> preparedBindingPlan(const CompiledVariant&
}
key.push_back(exports.size());
for (const auto mapping : exports) key.push_back(mapping);
static std::shared_mutex mutex;
static PrimedSharedMutex mutex;
static std::map<std::vector<std::uint64_t>, std::shared_ptr<PreparedBindingPlan>> plans;
std::shared_ptr<PreparedBindingPlan> plan;
{
@ -647,7 +655,7 @@ std::shared_ptr<const SpecializedModule> specializeModule(const CompiledShaderAr
key.clear();
key.push_back(artifact.variantId);
for (const auto& constant : constants) key.push_back((static_cast<std::uint64_t>(constant.id) << 32u) | constant.value);
static std::shared_mutex mutex;
static PrimedSharedMutex mutex;
static std::map<std::vector<std::uint64_t>, std::shared_ptr<SpecializedModuleEntry>> modules;
std::shared_ptr<SpecializedModuleEntry> entry;
{

View File

@ -7,7 +7,7 @@
- libgcc_s_seh-1.dll
- libstdc++-6.dll
- libwinpthread-1.dll
- winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. Guest rwlocks ([Pthread.hpp](../../core/libs/prx/libkernel/Pthread/include/Pthread.hpp)) are locked once when created. The host `std::shared_mutex` objects in [Recompiler.cpp](../../core/shader/recompiler/Recompiler.cpp) and [GuestWriteWatch.cpp](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) are not, and can still race on their first contended use.
- winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. Guest rwlocks ([Pthread.hpp](../../core/libs/prx/libkernel/Pthread/include/Pthread.hpp)) and the host `std::shared_mutex` objects in [Recompiler.cpp](../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same.
### Silent stubs