blob: 545b80b142f885ce5e515fcc709802f3a4e690b4 [file] [log] [blame]
/*
* Copyright 2025 Rive
*/
#pragma once
#include "rive/renderer/gpu.hpp"
#include "rive/renderer/render_context.hpp"
#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
namespace rive::gpu
{
enum class PipelineCreateType
{
sync,
async,
};
enum class PipelineStatus
{
notReady,
ready,
errored,
};
struct StandardPipelineProps
{
DrawType drawType;
ShaderFeatures shaderFeatures;
InterlockMode interlockMode;
ShaderMiscFlags shaderMiscFlags;
#ifdef WITH_RIVE_TOOLS
SynthesizedFailureType synthesizedFailureType =
SynthesizedFailureType::none;
#endif
uint32_t createKey() const
{
return gpu::ShaderUniqueKey(drawType,
shaderFeatures,
interlockMode,
shaderMiscFlags);
}
};
// Helper class to manage asynchronous creation of a pipeline (i.e. a
// combination of a vertex/pixel shader)
template <typename PipelineType> class AsyncPipelineManager
{
public:
using PipelineProps = typename PipelineType::PipelineProps;
using VertexShaderType = typename PipelineType::VertexShaderType;
using FragmentShaderType = typename PipelineType::FragmentShaderType;
// The pipeline key might be 32- or 64-bit depending on renderer, so detect
// which it is.
using PipelineKey = decltype(std::declval<PipelineProps>().createKey());
AsyncPipelineManager(ShaderCompilationMode mode) : m_mode(mode) {}
AsyncPipelineManager(const AsyncPipelineManager&) = delete;
AsyncPipelineManager& operator=(const AsyncPipelineManager&) = delete;
virtual ~AsyncPipelineManager()
{
// If we created a background thread, it's important for the derived
// class to have already called shutdownBackgroundThread before
// destructing its own internal state (otherwise the thread might be
// mid-shader-creation when it unloads its context).
assert(!m_jobThread.joinable());
}
const PipelineType* tryGetPipeline(const PipelineProps& propsIn,
const PlatformFeatures& platformFeatures)
{
PipelineProps props = propsIn;
ShaderFeatures ubershaderFeatures =
gpu::UbershaderFeaturesMaskFor(props.shaderFeatures,
props.drawType,
props.interlockMode,
platformFeatures);
PipelineCreateType createType;
switch (m_mode)
{
case ShaderCompilationMode::allowAsynchronous:
default:
// If this is an ubershader equivalent, we want to create this
// draw program synchronously so that it is available
// immediately
createType = (props.shaderFeatures == ubershaderFeatures)
? PipelineCreateType::sync
: PipelineCreateType::async;
break;
case ShaderCompilationMode::onlyUbershaders:
// For ubershader-only loading, we'll always use the full
// ubershader
// feature flags and always load synchronously.
props.shaderFeatures = ubershaderFeatures;
[[fallthrough]];
case ShaderCompilationMode::alwaysSynchronous:
createType = PipelineCreateType::sync;
break;
}
PipelineKey key = props.createKey();
auto iter = m_pipelines.find(key);
#ifdef WITH_RIVE_TOOLS
// If requested, synthesize a complete failure to get an ubershader
// (i.e. pretend we attempted to load the current shader asynchronously
// and tried to fall back on an uber, which failed) (Don't fail on
// "renderPassResolve" in atomic mode because if we fail that one the
// unit test won't see the clear color.)
if (props.synthesizedFailureType ==
gpu::SynthesizedFailureType::ubershaderLoad &&
props.drawType != DrawType::renderPassResolve)
{
return nullptr;
}
if (props.shaderFeatures == ubershaderFeatures)
{
// Otherwise, do not synthesize compilation failure for an
// ubershader.
props.synthesizedFailureType = gpu::SynthesizedFailureType::none;
}
#endif
if (iter == m_pipelines.end())
{
// We haven't encountered this shader key yet so it's time to ask
// the render context to create the program. If it's happening
// asynchronously, it might return us an incomplete pipeline
// (for, say, WebGL where there is background shader compilation),
// or it might return a nullptr/nullopt value (which means it
// queued the work into our jobQueue and we'll get it later)
auto pipeline = createPipeline(createType, key, props);
iter = m_pipelines.insert({key, std::move(pipeline)}).first;
if (createType == PipelineCreateType::sync)
{
// If we asked to create a synchronous one, return it
// imemdiately (regardless of whether background compilation
// has finished or not - the render context is in charge of
// stalling on an incomplete shader when it gets one)
assert(iter->second);
if (getPipelineStatus(*iter->second) != PipelineStatus::errored)
{
return &*iter->second;
}
if (props.shaderFeatures == ubershaderFeatures)
{
// Ubershader creation failed
#ifdef WITH_RIVE_TOOLS
assert(props.synthesizedFailureType !=
SynthesizedFailureType::none);
#else
assert(false && "Ubershader creation failed");
#endif
return nullptr;
}
// This pipeline failed to build for some reason, but we can
// (potentially) fall back on the ubershader.
assert(props.shaderFeatures != ubershaderFeatures);
}
}
if (!iter->second)
{
// We don't have this shader yet, so run through the list of
// completed shaders and see
CompletedJob completedJob;
while (popCompletedJob(&completedJob))
{
m_pipelines[completedJob.key] = std::move(completedJob.program);
if (completedJob.key == key)
{
assert(iter->second);
break;
}
}
}
if (iter->second)
{
if (auto status = getPipelineStatus(*iter->second);
status == PipelineStatus::ready)
{
// The program is present and ready to go!
return &*iter->second;
}
else if (status != PipelineStatus::errored)
{
assert(status == PipelineStatus::notReady);
// The program was not ready yet so attempt to move its creation
// process forward.
if (advanceCreation(*iter->second, props))
{
// The program was not previously ready, but it is now.
return &*iter->second;
}
}
}
if (props.shaderFeatures == ubershaderFeatures)
{
// The only way to get here for an ubershader should be for it to
// have failed to compile.
assert(iter->second &&
getPipelineStatus(*iter->second) == PipelineStatus::errored);
return nullptr;
}
// The pipeline is still not ready, so instead return an ubershader
// version (with all functionality enabled). This will create
// synchronously so we're guaranteed to have a valid return from this
// call.
assert(props.shaderFeatures != ubershaderFeatures);
auto ubershaderProps = props;
ubershaderProps.shaderFeatures = ubershaderFeatures;
return tryGetPipeline(ubershaderProps, platformFeatures);
}
const VertexShaderType& getVertexShaderSynchronous(
DrawType drawType,
ShaderFeatures shaderFeatures,
InterlockMode interlockMode)
{
// Remove any non-vertex-shader features before doing the key
shaderFeatures &= kVertexShaderFeaturesMask;
return getSharedObjectSynchronous(
gpu::ShaderUniqueKey(drawType,
shaderFeatures,
interlockMode,
ShaderMiscFlags::none),
m_vertexShaderMap,
[&]() {
return createVertexShader(drawType,
shaderFeatures,
interlockMode);
});
}
const FragmentShaderType& getFragmentShaderSynchronous(
DrawType drawType,
ShaderFeatures shaderFeatures,
InterlockMode interlockMode,
ShaderMiscFlags miscFlags)
{
return getSharedObjectSynchronous(gpu::ShaderUniqueKey(drawType,
shaderFeatures,
interlockMode,
miscFlags),
m_fragmentShaderMap,
[&]() {
return createFragmentShader(
drawType,
shaderFeatures,
interlockMode,
miscFlags);
});
}
void clearCache()
{
std::unique_lock lock{m_mutex};
// Start by clearing the job queue (There's no reset or clear on
// std::queue so we have to pop manually). Doing it this way instead of
// doing "m_jobQueue = {}" to keep the internal buffer intact and avoid
// some heap allocations the next time we start queueing jobs again
while (!m_jobQueue.empty())
{
m_jobQueue.pop();
}
// Now wait for the background thread(s) to finish any work they are
// actively doing.
while (m_activePipelineCreationCount > 0)
{
m_jobCompleteCV.wait(lock);
}
// Clear all of the rest of our cached everything - we'll start over.
m_completedJobs.clear();
m_vertexShaderMap.clear();
m_fragmentShaderMap.clear();
m_pipelines.clear();
clearCacheInternal();
}
protected:
virtual std::unique_ptr<VertexShaderType> createVertexShader(
DrawType,
ShaderFeatures,
InterlockMode) = 0;
virtual std::unique_ptr<FragmentShaderType> createFragmentShader(
DrawType,
ShaderFeatures,
InterlockMode,
ShaderMiscFlags) = 0;
// This function is called to create a pipeline when we don't have one
// with its key already. It can be called in either "sync" or "async" mode.
// - In async mode, the function should either:
// - call queueBackgroundJob with the supplied parameters so that it will
// be processed by the background thread (like D3D does)
// - create the shader using the target API's built-in async/parallel
// thread creation model (like GL does)
// - In sync mode, the shader should be created immediately. This mode is
// used either when compiling the fallback "ubershader" version of a
// drawType (as it is required immediately), or when the background job
// thread wants to create a shader (so the implementation of sync mode
// needs to be thread-safe when using the target API in that case)
virtual std::unique_ptr<PipelineType> createPipeline(
PipelineCreateType,
PipelineKey key,
const PipelineProps&) = 0;
// For renderers like GL that have a polling/step-based async setup,
// override this function to return whether or not the pipeline is
// fully ready or still loading.
virtual PipelineStatus getPipelineStatus(const PipelineType&) const = 0;
// For renderers like GL that have a step-based async setup,
// override this function to attempt to move the shader along its progress.
virtual bool advanceCreation(PipelineType&, const PipelineProps&)
{
return false; // do nothing by default
}
// If renderers have extra state, this is where they can clear it while
// within the safety of the mutex lock
virtual void clearCacheInternal() {}
// Called by the render context to use the background threading model to
// create pipeline
void queueBackgroundJob(PipelineKey key, const PipelineProps& props)
{
// start the job thread if we haven't already
if (!m_jobThread.joinable())
{
m_jobThread =
std::thread{[this] { backgroundShaderCompilationThread(); }};
}
std::unique_lock lock{m_mutex};
m_jobQueue.push({props, key});
m_newJobCV.notify_one();
}
void shutdownBackgroundThread()
{
if (m_jobThread.joinable())
{
{
std::unique_lock lock{m_mutex};
m_isDone = true;
}
m_newJobCV.notify_all();
m_jobThread.join();
}
}
template <typename KeyType, typename SharedObject, typename CreateFunc>
SharedObject& getSharedObjectSynchronous(
KeyType key,
std::unordered_map<uint32_t, std::unique_ptr<SharedObject>>& map,
CreateFunc&& createFunc)
{
// See if the object exists already first
{
std::unique_lock lock{m_mutex};
auto iter = map.find(key);
if (iter != map.end())
{
while (iter->second == nullptr)
{
// This is either a synchronous object request and an
// asynchronous build is running it *or* it's on an async
// thread and another thread is running it - either way we
// need to wait for the thread making this object to finish
// (so we only build it once)
m_sharedObjectReadyCV.wait(lock);
}
return *iter->second;
}
// Insert an empty entry into the object map so the other threads
// don't try to double-up on creation of this object
map.try_emplace(key);
}
// Now that we're outside of the lock, ask the renderer context to
// create the object
auto object = createFunc();
{
std::unique_lock lock{m_mutex};
auto iter = map.find(key);
assert(iter != map.end());
assert(iter->second == nullptr);
iter->second = std::move(object);
m_sharedObjectReadyCV.notify_all();
return *iter->second;
}
}
private:
struct JobParams
{
PipelineProps props;
PipelineKey key;
};
struct CompletedJob
{
PipelineKey key;
std::unique_ptr<PipelineType> program;
};
bool popCompletedJob(CompletedJob* jobOut)
{
std::lock_guard lock{m_mutex};
if (m_completedJobs.empty())
{
return false;
}
*jobOut = std::move(m_completedJobs.back());
m_completedJobs.pop_back();
return true;
}
void backgroundShaderCompilationThread()
{
while (true)
{
JobParams nextJob;
{
std::unique_lock lock{m_mutex};
while (!m_isDone && m_jobQueue.empty())
{
m_newJobCV.wait(lock);
}
if (m_isDone)
{
return;
}
nextJob = std::move(m_jobQueue.front());
m_jobQueue.pop();
m_activePipelineCreationCount++;
}
auto newPipeline = createPipeline(PipelineCreateType::sync,
nextJob.key,
nextJob.props);
{
std::unique_lock lock{m_mutex};
m_completedJobs.push_back({
nextJob.key,
std::move(newPipeline),
});
m_activePipelineCreationCount--;
m_jobCompleteCV.notify_all();
}
}
}
std::unordered_map<uint32_t, std::unique_ptr<VertexShaderType>>
m_vertexShaderMap;
std::unordered_map<uint32_t, std::unique_ptr<FragmentShaderType>>
m_fragmentShaderMap;
std::unordered_map<PipelineKey, std::unique_ptr<PipelineType>> m_pipelines;
std::queue<JobParams> m_jobQueue;
std::vector<CompletedJob> m_completedJobs;
bool m_isDone = false;
uint32_t m_activePipelineCreationCount = 0;
const ShaderCompilationMode m_mode = ShaderCompilationMode::standard;
std::thread m_jobThread;
std::mutex m_mutex;
std::condition_variable m_newJobCV;
std::condition_variable m_jobCompleteCV;
std::condition_variable m_sharedObjectReadyCV;
};
} // namespace rive::gpu