Files
plezy/linux/runner/mpv/plane_render_executor.cc
T
edde746 0fdc22b4fa fix(linux): make the render-executor completion handoff visible to TSan
GLib's GMutex is a raw futex, so ThreadSanitizer cannot see the
happens-before edge g_main_context_invoke_full provides and fails the
thread lane on plane_render_executor_test. Publish the completion
payload through an explicit release/acquire pair, and suppress the
remaining reports that originate wholly inside libglib's own source
bookkeeping. Races on repository memory still fail the run; verified
0/100 failing TSan runs in an ubuntu:24.04 container (20/20 before).
2026-08-24 12:42:23 +02:00

117 lines
3.9 KiB
C++

#include "plane_render_executor.h"
#include <atomic>
#include <chrono>
namespace mpv {
namespace {
struct CompletionInvocation {
PlaneRenderExecutor::Completion completion;
bool result;
// The worker hands this struct to the main context through
// g_main_context_invoke_full, whose internal context lock is a GLib futex
// that ThreadSanitizer cannot observe. This release/acquire pair republishes
// that same happens-before edge in a form TSan models; GLib still provides
// the delivery ordering.
std::atomic<bool> published{false};
};
gboolean InvokeCompletion(gpointer data) {
auto* invocation = static_cast<CompletionInvocation*>(data);
// Pairs with the release store in Run(); see CompletionInvocation.
(void)invocation->published.load(std::memory_order_acquire);
invocation->completion(invocation->result);
return G_SOURCE_REMOVE;
}
void DestroyCompletionInvocation(gpointer data) {
auto* invocation = static_cast<CompletionInvocation*>(data);
// The destroy notify can also be the first main-thread access when the
// source is torn down without dispatching; take the same edge.
(void)invocation->published.load(std::memory_order_acquire);
delete invocation;
}
} // namespace
PlaneRenderExecutor::Shared::~Shared() {
if (completion_context != nullptr) g_main_context_unref(completion_context);
}
PlaneRenderExecutor::PlaneRenderExecutor() : shared_(std::make_shared<Shared>()) {
shared_->completion_context = g_main_context_ref_thread_default();
thread_ = std::thread(&PlaneRenderExecutor::Run, shared_);
}
PlaneRenderExecutor::~PlaneRenderExecutor() { ShutdownAndJoin(5000); }
bool PlaneRenderExecutor::Post(Job job, Completion completion) {
{
std::lock_guard<std::mutex> lock(shared_->mutex);
if (shared_->quitting) return false;
shared_->jobs.emplace_back(std::move(job), std::move(completion));
}
shared_->wake.notify_one();
return true;
}
bool PlaneRenderExecutor::ShutdownAndJoin(unsigned int timeout_ms) {
if (!thread_.joinable()) return !abandoned_;
{
std::unique_lock<std::mutex> lock(shared_->mutex);
shared_->quitting = true;
shared_->wake.notify_all();
if (!shared_->idle.wait_for(lock, std::chrono::milliseconds(timeout_ms), [this] {
return shared_->jobs.empty() && !shared_->running_job;
})) {
abandoned_ = true;
}
}
if (abandoned_) {
// The job is wedged inside a call that cannot be interrupted. Joining
// would hang the caller forever; the thread is cut loose instead, and the
// caller is told so it can leak, rather than free, whatever the job may
// still be touching. The worker keeps the shared state alive on its own.
thread_.detach();
return false;
}
thread_.join();
return true;
}
void PlaneRenderExecutor::Run(const std::shared_ptr<Shared>& shared) {
for (;;) {
Job job;
Completion completion;
{
std::unique_lock<std::mutex> lock(shared->mutex);
shared->wake.wait(lock, [&shared] { return !shared->jobs.empty() || shared->quitting; });
// Quitting drains: queued jobs still run, so a shutdown-time job (the
// EGL unbind) can be posted and then waited for.
if (shared->jobs.empty()) return;
job = std::move(shared->jobs.front().first);
completion = std::move(shared->jobs.front().second);
shared->jobs.pop_front();
shared->running_job = true;
}
const bool result = job ? job() : false;
if (completion) {
auto* invocation = new CompletionInvocation{std::move(completion), result};
invocation->published.store(true, std::memory_order_release);
g_main_context_invoke_full(
shared->completion_context, G_PRIORITY_DEFAULT, InvokeCompletion, invocation, DestroyCompletionInvocation);
}
{
std::lock_guard<std::mutex> lock(shared->mutex);
shared->running_job = false;
}
shared->idle.notify_all();
}
}
} // namespace mpv