blob: 2bbd5cd08d10a791c2d78c2843bf38dfd57dc931 [file]
/*
* Copyright 2025 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/gpu/graphite/PipelineManager.h"
#include "include/private/SkLog.h"
#include "src/core/SkTaskGroup.h"
#include "src/gpu/graphite/GraphicsPipelineDesc.h"
#include "src/gpu/graphite/GraphicsPipelineHandle.h"
#include "src/gpu/graphite/PipelineCreationTask.h"
#include "src/gpu/graphite/RenderPassDesc.h"
#include "src/gpu/graphite/RuntimeEffectDictionary.h"
#include "src/gpu/graphite/SharedContext.h"
// The threaded PipelineManager is set up to work with a executor with two worklists.
// All work from the first work list must be completed before work from the second list
// is begun. In-line compiles will be put in the high priority list while precompiles
// will be put in the low priority work list. If a single work list executor is provided
// everything will collapse to just being interleaved on that single work list.
// Note that the number of worklists is orthogonal to the number of threads.
static constexpr int kHighPriorityWorkList = 0;
static constexpr int kLowPriorityWorkList = 1;
namespace skgpu::graphite {
// Directly accessing the two variants is thread safe because a given GraphicsPipelineHandle
// does not switch between a task or pipeline when the task completes. The 'fCompleted' check
// is atomic and, if the task is completed, the 'fPipeline' access is thread safe. There is,
// obviously, an inherent race wrt the 'fCompleted' access. Callers must either ensure that
// the Handle has already been resolved or accept some raciness in the response.
sk_sp<GraphicsPipeline> GraphicsPipelineHandle::pipelineOrNull() const {
if (std::holds_alternative<sk_sp<GraphicsPipeline>>(fTaskOrPipeline)) {
return std::get<sk_sp<GraphicsPipeline>>(fTaskOrPipeline);
}
sk_sp<PipelineCreationTask> task = std::get<sk_sp<PipelineCreationTask>>(fTaskOrPipeline);
if (!task->fCompleted) {
return nullptr;
}
return task->fPipeline;
}
GraphicsPipelineHandle::GraphicsPipelineHandle(sk_sp<PipelineCreationTask> task)
: fTaskOrPipeline(std::move(task)) {}
GraphicsPipelineHandle::GraphicsPipelineHandle(sk_sp<GraphicsPipeline> pipeline)
: fTaskOrPipeline(std::move(pipeline)) {}
PipelineManager::PipelineManager(SkExecutor* executor) {
if (executor) {
fTaskGroup = std::make_unique<SkTaskGroup>(*executor);
}
}
PipelineManager::~PipelineManager() {
// The task group should've been shutdown and deleted in Context's destructor (via
// shutDown()).
{
SkDEBUGCODE(SkAutoSpinlock lock{fSpinLock};)
SkASSERT(!fTaskGroup);
SkASSERT(fActiveTasks.count() == 0);
}
}
const UniqueKey& PipelineManager::Traits::GetKey(const sk_sp<PipelineCreationTask>& task) {
return task->fPipelineKey;
}
uint32_t PipelineManager::Traits::Hash(const UniqueKey& pipelineKey) {
return pipelineKey.hash();
}
GraphicsPipelineHandle PipelineManager::createHandle(
SharedContext* sharedContext,
sk_sp<const RuntimeEffectDictionary> runtimeDict,
const GraphicsPipelineDesc& pipelineDesc,
const RenderPassDesc& renderPassDesc,
SkEnumBitMask<PipelineCreationFlags> pipelineCreationFlags) {
GlobalCache* globalCache = sharedContext->globalCache();
const Caps* caps = sharedContext->caps();
bool forPrecompile =
SkToBool(pipelineCreationFlags & PipelineCreationFlags::kForPrecompilation);
Priority curPriority = forPrecompile ? Priority::kLow : Priority::kHigh;
UniqueKey pipelineKey = caps->makeGraphicsPipelineKey(pipelineDesc, renderPassDesc);
sk_sp<GraphicsPipeline> pipeline = globalCache->findGraphicsPipeline(
pipelineKey,
pipelineCreationFlags);
if (pipeline) {
return GraphicsPipelineHandle(std::move(pipeline));
}
// Although 'findGraphicsPipeline' didn't find a GraphicsPipeline, there could be a race.
// In 'findOrCreateTask' we will, thread-safely, check if there is a task in flight to
// create the Pipeline and, failing that, create one. If the race had occurred and
// there is actually a matching GraphicsPipeline in the GlobalCache then it will be found
// in 'compileTask'.
sk_sp<PipelineCreationTask> task = this->findOrCreateTask(std::move(runtimeDict),
pipelineKey,
pipelineDesc,
renderPassDesc,
curPriority);
bool shouldAddToWorkList = false;
if (curPriority == Priority::kHigh && task->isLowPriority()) {
// If we found an active task for the current Pipeline we know, modulo thread races,
// that it hasn't completed yet (since it, then, wouldn't be in the active task list).
// If it was initially added as low priority, but turned out to be high priority,
// re-add it as a high priority task.
shouldAddToWorkList = !task->fIsHighPriority.exchange(true);
} else {
// Tasks are only removed from the TaskList when they are complete. This means that
// an in-flight task can be found and resubmitted for compilation. The 'fInWorkList'
// guard ensures we don't resubmit the same task over and over (modulo the one-off
// switch from low to high priority above).
shouldAddToWorkList = !task->fInWorkList.exchange(true);
}
if (shouldAddToWorkList) {
this->addTaskToWorkList(sharedContext, task, curPriority);
}
return GraphicsPipelineHandle(std::move(task));
}
void PipelineManager::InlineCompile(SharedContext* sharedContext,
PipelineManager* pipelineManager,
PipelineCreationTask* task) {
// This is a bit racy but the exact correctness of the actual cause for the compilation
// isn't crucial. In essence, this tries to give the SharedContext a best guess about
// the driver behind the compilation. The exact race is if a precompile compilation
// was usurped by a normal compilation but we still report a precompile compilation
// to the SharedContext.
PipelineCreationFlags flags = task->isLowPriority()
? PipelineCreationFlags::kForPrecompilation
: PipelineCreationFlags::kNone;
task->fPipeline = sharedContext->findOrCreateGraphicsPipeline(
task->fRuntimeDict.get(),
task->fPipelineKey,
task->fGraphicsPipelineDesc,
task->fRenderPassDesc,
flags);
if (!task->fPipeline) {
SKIA_LOG_W("Failed to create GraphicsPipeline!");
}
pipelineManager->signalCompleted(task);
pipelineManager->removeTask(task);
}
void PipelineManager::addTaskToWorkList(SharedContext* sharedContext,
sk_sp<PipelineCreationTask> task,
Priority priority) {
// Note: this lambda function relies on the continued existence of the shared
// context and the PipelineManager. The task is reffed.
auto compileTask = [sharedContext, this, task] {
// Since there might be threaded contention to execute the compilation for the same
// task (e.g., if a low priority compile got duplicated as a high priority compile
// or an immediate compile was required), we check the 'fStarted' atomic so only
// one does the work.
if (task->fStarted.exchange(true)) {
return;
}
InlineCompile(sharedContext, this, task.get());
};
{
SkAutoSpinlock lock{fSpinLock};
if (fTaskGroup) {
int workList = priority == Priority::kLow ? kLowPriorityWorkList
: kHighPriorityWorkList;
fTaskGroup->add(std::move(compileTask), workList);
return;
}
}
// Non-SkExecutor fallback. Note that, if multiple Recorders are recording in parallel on
// multiple threads (w/ no SkExecutor supplied) there could still be a compilation race
// here. In that case all the thread-safety mechanisms (e.g., 'fStarted', 'fCompleted')
// will kick in to eliminate duplicate work. This does mean, as in the SkExecutor case,
// that the task's Pipeline need not be resolved at the end of 'compileTask'. That is,
// after all, the purview of 'resolveHandle'.
compileTask();
}
sk_sp<GraphicsPipeline> PipelineManager::resolveHandle(SharedContext* sharedContext,
const GraphicsPipelineHandle& handle) {
if (std::holds_alternative<sk_sp<GraphicsPipeline>>(handle.fTaskOrPipeline)) {
return std::get<sk_sp<GraphicsPipeline>>(handle.fTaskOrPipeline);
}
// Since 'fTaskOrPipeline' doesn't hold a pipeline the pipeline must not have existed when
// the handle was created so a compilation task must've been created to compile it
sk_sp<PipelineCreationTask> task =
std::get<sk_sp<PipelineCreationTask>>(handle.fTaskOrPipeline);
// For the non-threaded PipelineManager, the GraphicsPipeline will have been compiled in-line
// so will already have been completed.
this->potentiallyWaitOn(sharedContext, task.get());
return task->fPipeline;
}
void PipelineManager::shutDown() {
// We null out 'fTaskGroup' so no more threaded work can be added after this point.
std::unique_ptr<SkTaskGroup> tmp;
{
SkAutoSpinlock lock{fSpinLock};
tmp = std::move(fTaskGroup);
}
if (tmp) {
// We have to wait for the remaining tasks to complete bc they rely on the existence
// of the SharedContext and the PipelineManager (this).
// TODO(robertphillips) We could discard any unstarted tasks but would need a way to
// have them still remove themselves from the task list.
tmp->wait();
}
{
SkDEBUGCODE(SkAutoSpinlock lock{fSpinLock};)
SkASSERT(!fTaskGroup);
SkASSERT(fActiveTasks.count() == 0);
}
}
#if defined(GPU_TEST_UTILS)
void PipelineManager::wait_TestOnly() {
SkTaskGroup* tmp;
{
SkAutoSpinlock lock{fSpinLock};
tmp = fTaskGroup.get();
}
// This isn't safe (since 'fTaskGroup' could be altered on some other thread) but, hopefully,
// the unit tests know what they're doing (i.e., don't delete the owning Context while
// in this method).
if (tmp) {
tmp->wait();
}
}
PipelineManager::Stats PipelineManager::getStats() const {
SkAutoSpinlock lock{fSpinLock};
return fStats;
}
#endif
sk_sp<PipelineCreationTask> PipelineManager::findOrCreateTask(
sk_sp<const RuntimeEffectDictionary> runtimeDict,
const UniqueKey& pipelineKey,
const GraphicsPipelineDesc& pipelineDesc,
const RenderPassDesc& renderPassDesc,
Priority priority) {
SkAutoSpinlock lock{fSpinLock};
sk_sp<PipelineCreationTask>* task = fActiveTasks.find(pipelineKey);
if (task) {
#if defined(GPU_TEST_UTILS)
fStats.fNumPreemptivelyFoundTasks++;
#endif
return *task;
}
#if defined(GPU_TEST_UTILS)
fStats.fNumTasksCreated++;
#endif
sk_sp<PipelineCreationTask> newTask = sk_sp<PipelineCreationTask>(
new PipelineCreationTask(std::move(runtimeDict),
pipelineKey,
pipelineDesc,
renderPassDesc,
priority == Priority::kHigh));
fActiveTasks.set(newTask);
return newTask;
}
void PipelineManager::removeTask(PipelineCreationTask* task) {
SkAutoSpinlock lock{fSpinLock};
fActiveTasks.remove(task->fPipelineKey);
}
void PipelineManager::signalCompleted(PipelineCreationTask* task) {
std::unique_lock<std::mutex> lock(fMutex);
// Even though 'fCompleted' is atomic it is still required that it be
// modified within the locked mutex lest the 'wait' in potentiallyWaitOn
// misses the signal.
task->fCompleted = true;
lock.unlock();
// potentiallyWaitOn should only ever be called from the main thread (on which
// Context::insertRecording is called) so only one thread should ever be waiting
fConditionVariable.notify_one();
}
void PipelineManager::potentiallyWaitOn(SharedContext* sharedContext, PipelineCreationTask* task) {
// If we can preempt some thread that is scheduled to compile this Pipeline, do so rather
// than waiting.
if (!task->fStarted.exchange(true)) {
InlineCompile(sharedContext, this, task);
SkASSERT(task->fCompleted);
return;
}
std::unique_lock<std::mutex> lock(fMutex);
if (task->fCompleted) {
return;
}
fConditionVariable.wait(lock, [task]{ return task->fCompleted.load(); });
SkASSERT(task->fCompleted);
}
} // namespace skgpu::graphite