GL: programs compiled on the driver's threads, drawn with once linked

Where the driver compiles on threads of its own
(GL_KHR_parallel_shader_compile or the ARB one), the asynchronous
shader modes on OpenGL compile in the background on the frontend's
context alone: a missing program's compile and link are started, a
couple of work items a frame, and its draws use the ubershaders, or
are skipped, until GL_COMPLETION_STATUS says the link is done; its
link is checked, its variables set and its binary written to the cache
then. A program drawn with before it is done is waited for there. A
software rasterizer, which compiles when it first draws, compiles on
the spot as before.

The shader compiler gets a deferred mode for it, without threads,
compiling the items waiting a few at a time, and AbstractPipeline tells
whether a pipeline's compile is still running and whether it can be
drawn with. With the driver's threads alone, every ubershader pipeline
there can be is not built in the background.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-11-07 02:05:36 +01:00
1 parent a390228b51
commit bc09a84dd1
13 files changed
+183 -22

No files matched your search

@@ -328,6 +328,29 @@ bool PopulateConfig(GLContext* m_main_gl_context)
g_ogl_config.bSupportsGLBufferStorage = GLExtensions::Supports("GL_ARB_buffer_storage") ||
GLExtensions::Supports("GL_EXT_buffer_storage");
g_ogl_config.bSupportsMSAA = GLExtensions::Supports("GL_ARB_texture_multisample");
{
/* as many threads as the driver will have; not on a software
* rasterizer, which compiles when it first draws, where a program
* compiled in the background only leaves the ubershaders drawing */
const std::string_view renderer = g_ogl_config.gl_renderer;
const bool software = renderer.find("llvmpipe") != std::string_view::npos ||
renderer.find("softpipe") != std::string_view::npos ||
renderer.find("GDI Generic") != std::string_view::npos;
void (*max_threads)(GLuint) = nullptr;
if (!software && GLExtensions::Supports("GL_KHR_parallel_shader_compile"))
{
max_threads = reinterpret_cast<void (*)(GLuint)>(
m_main_gl_context->GetFuncAddress("glMaxShaderCompilerThreadsKHR"));
}
else if (!software && GLExtensions::Supports("GL_ARB_parallel_shader_compile"))
{
max_threads = reinterpret_cast<void (*)(GLuint)>(
m_main_gl_context->GetFuncAddress("glMaxShaderCompilerThreadsARB"));
}
g_ogl_config.bSupportsParallelShaderCompile = max_threads != nullptr;
if (max_threads)
max_threads(0xFFFFFFFFu);
}
g_ogl_config.bSupportViewportFloat = GLExtensions::Supports("GL_ARB_viewport_array");
g_ogl_config.bSupportsDebug =
GLExtensions::Supports("GL_KHR_debug") || GLExtensions::Supports("GL_ARB_debug_output");
@@ -73,6 +73,9 @@ struct VideoConfig
bool bSupportsAniso;
bool bSupportsBitfield;
bool bSupportsTextureSubImage;
// The driver compiles and links on threads of its own (GL_KHR/ARB_parallel_shader_compile),
// told done with GL_COMPLETION_STATUS.
bool bSupportsParallelShaderCompile;
EsFbFetchType SupportedFramebufferFetch;
bool bSupportsKHRShaderSubgroup; // basic + arithmetic + ballot
bool bSupportsExplicitLayoutInShader;
+11 -2
View File
@@ -599,7 +599,11 @@ void OGLGfx::SetPipeline(const AbstractPipeline* pipeline)
ApplyBlendingState(static_cast<const OGLPipeline*>(pipeline)->GetBlendingState());
ProgramShaderCache::BindVertexFormat(
static_cast<const OGLPipeline*>(pipeline)->GetVertexFormat());
static_cast<const OGLPipeline*>(pipeline)->GetProgram()->shader.Bind();
/* a program still linking is waited for here, the first time it is drawn with */
PipelineProgram* program = const_cast<PipelineProgram*>(
static_cast<const OGLPipeline*>(pipeline)->GetProgram());
ProgramShaderCache::FinishLink(program, true);
program->shader.Bind();
}
else
{
@@ -671,7 +675,12 @@ void OGLGfx::UnbindTexture(const AbstractTexture* texture)
std::unique_ptr<VideoCommon::AsyncShaderCompiler> OGLGfx::CreateAsyncShaderCompiler()
{
return std::make_unique<VideoCommon::AsyncShaderCompiler>();
std::unique_ptr<VideoCommon::AsyncShaderCompiler> compiler =
std::make_unique<VideoCommon::AsyncShaderCompiler>();
/* the driver compiles on threads of its own: programs are started a few a frame and drawn
* with once linked */
compiler->SetDeferred(g_ogl_config.bSupportsParallelShaderCompile);
return compiler;
}
bool OGLGfx::IsGLES() const
@@ -50,8 +50,11 @@ AbstractPipeline::CacheData OGLPipeline::GetCacheData() const
// copies of the same program combination, we set a flag on the program object so that it can't
// be retrieved again. When booting, the pipeline cache is loaded in-order, so the additional
// pipelines which use the program combination will re-use the already-created object.
if (!g_backend_info.bSupportsPipelineCacheData || m_program->binary_retrieved)
if (!g_backend_info.bSupportsPipelineCacheData || m_program->binary_retrieved ||
!ProgramShaderCache::FinishLink(m_program, true))
{
return {};
}
GLint program_size = 0;
glGetProgramiv(m_program->shader.glprogid, GL_PROGRAM_BINARY_LENGTH, &program_size);
@@ -27,6 +27,12 @@ public:
bool HasVertexInput() const { return m_vertex_format != nullptr; }
GLenum GetGLPrimitive() const { return m_gl_primitive; }
CacheData GetCacheData() const override;
bool IsReady() const override { return ProgramShaderCache::FinishLink(m_program, false); }
bool IsBuilding() const override
{
ProgramShaderCache::FinishLink(m_program, false);
return m_program->link_pending;
}
static std::unique_ptr<OGLPipeline> Create(const AbstractPipelineConfig& config,
const void* cache_data, size_t cache_data_size);
@@ -322,6 +322,10 @@ GLuint ProgramShaderCache::CompileSingleShader(GLenum type, std::string_view cod
glShaderSource(result, num_strings, src.data(), src_sizes.data());
glCompileShader(result);
/* compiled on the driver's threads: an error shows when the program links */
if (g_ogl_config.bSupportsParallelShaderCompile)
return result;
if (!CheckShaderCompileResult(result, type, code))
{
// Don't try to use this shader
@@ -332,6 +336,27 @@ GLuint ProgramShaderCache::CompileSingleShader(GLenum type, std::string_view cod
return result;
}
bool ProgramShaderCache::FinishLink(PipelineProgram* prog, bool wait)
{
if (!prog->link_pending)
return !prog->link_failed;
if (!wait)
{
GLint done = GL_FALSE;
glGetProgramiv(prog->shader.glprogid, 0x91B1 /* GL_COMPLETION_STATUS_KHR */, &done);
if (done != GL_TRUE)
return false;
}
prog->link_pending = false;
if (!CheckProgramLinkResult(prog->shader.glprogid, {}, {}, {}))
{
prog->link_failed = true;
return false;
}
prog->shader.SetProgramVariables();
return true;
}
bool ProgramShaderCache::CheckShaderCompileResult(GLuint id, GLenum type, std::string_view code)
{
GLint compileStatus;
@@ -610,10 +635,14 @@ PipelineProgram* ProgramShaderCache::GetPipelineProgram(const GLVertexFormat* ve
if (vao != s_last_VAO)
glBindVertexArray(s_last_VAO);
if (!CheckProgramLinkResult(prog->shader.glprogid,
vertex_shader ? vertex_shader->GetSource() : std::string_view{},
geometry_shader ? geometry_shader->GetSource() : std::string_view{},
pixel_shader ? pixel_shader->GetSource() : std::string_view{}))
/* linked on the driver's threads: checked once done, FinishLink */
if (g_ogl_config.bSupportsParallelShaderCompile)
prog->link_pending = true;
else if (!CheckProgramLinkResult(prog->shader.glprogid,
vertex_shader ? vertex_shader->GetSource() : std::string_view{},
geometry_shader ? geometry_shader->GetSource() :
std::string_view{},
pixel_shader ? pixel_shader->GetSource() : std::string_view{}))
{
prog->shader.Destroy();
return nullptr;
@@ -622,7 +651,8 @@ PipelineProgram* ProgramShaderCache::GetPipelineProgram(const GLVertexFormat* ve
// Set program variables on the shader which will be returned.
// This is only needed for drivers which don't support binding layout.
prog->shader.SetProgramVariables();
if (!prog->link_pending)
prog->shader.SetProgramVariables();
// Publish it, unless another thread made the same program meanwhile; a dead program in the
// slot is simply replaced.
@@ -60,6 +60,9 @@ struct PipelineProgram
SHADER shader;
retro_atomic_int_t reference_count{1};
bool binary_retrieved = false;
// Linked on the driver's threads: checked, and its variables set, once done (FinishLink).
bool link_pending = false;
bool link_failed = false;
};
class ProgramShaderCache
@@ -73,6 +76,9 @@ public:
static void InvalidateLastProgram();
static bool CompileComputeShader(SHADER& shader, std::string_view code);
// A program linked on the driver's threads, checked once its link is done: true when it can
// be drawn with. With wait, waits for the link; without, false while it runs.
static bool FinishLink(PipelineProgram* prog, bool wait);
static GLuint CompileSingleShader(GLenum type, std::string_view code);
static bool CheckShaderCompileResult(GLuint id, GLenum type, std::string_view code);
static bool CheckProgramLinkResult(GLuint id, std::string_view vcode, std::string_view pcode,
@@ -85,4 +85,10 @@ public:
// pipeline objects, the cache is optionally used by the driver to speed up compilation.
using CacheData = std::vector<u8>;
virtual CacheData GetCacheData() const { return {}; }
// Whether the pipeline can be drawn with without waiting for its compile, which a driver may
// still be running on threads of its own. Asking does not wait.
virtual bool IsReady() const { return true; }
// Whether its compile is still running; once not, IsReady tells whether it succeeded.
virtual bool IsBuilding() const { return false; }
};
@@ -40,8 +40,12 @@ bool AsyncShaderCompiler::WaitForWorkDone()
void AsyncShaderCompiler::QueueWorkItem(WorkItemPtr item, u32 priority)
{
// If no worker threads are available, compile synchronously.
if (!HasWorkerThreads())
// If no worker threads are available, compile synchronously, or later when deferred.
if (IsDeferred())
{
m_pending_work.emplace(priority, std::move(item));
}
else if (!HasWorkerThreads())
{
item->Compile();
m_completed_sync.push_back(std::move(item));
@@ -91,6 +95,20 @@ size_t AsyncShaderCompiler::ItemsInSlots() const
void AsyncShaderCompiler::RetrieveWorkItems()
{
if (IsDeferred())
{
/* the most urgent few; an item may queue others, which wait their turn */
for (size_t i = 0; i < DEFERRED_ITEMS_PER_RETRIEVE && !m_pending_work.empty(); i++)
{
auto iter = m_pending_work.begin();
WorkItemPtr item = std::move(iter->second);
m_pending_work.erase(iter);
item->Compile();
item->Retrieve();
}
return;
}
// Workers that finished take their next items now, before this thread runs the retrievals.
FeedWorkers();
@@ -147,6 +165,13 @@ bool AsyncShaderCompiler::WaitUntilCompletion(
if (!HasPendingWork())
return true;
if (IsDeferred())
{
while (!m_pending_work.empty())
RetrieveWorkItems();
return true;
}
// Grab the number of pending items. We use this to work out how many are left.
const size_t total_items = m_completed_sync.size() + m_pending_work.size() + ItemsInSlots() +
retro_atomic_load_acquire_int(&m_busy_workers) + 1;
@@ -57,6 +57,11 @@ public:
// Needed because of calling virtual methods in shutdown procedure.
bool StartWorkerThreads(u32 num_worker_threads);
// Without worker threads, queued items wait and RetrieveWorkItems compiles a few of them each
// time it is called, for a backend whose compiles only start work the driver does on threads
// of its own (its pipelines then tell when they are ready, AbstractPipeline::IsReady).
void SetDeferred(bool deferred) { m_deferred = deferred; }
bool IsDeferred() const { return m_deferred && !HasWorkerThreads(); }
bool ResizeWorkerThreads(u32 num_worker_threads);
bool HasWorkerThreads() const;
void StopWorkerThreads();
@@ -116,6 +121,9 @@ private:
mpsc_stack_t m_completed_work{};
// Items finished synchronously, when there are no workers. Video thread only.
std::vector<WorkItemPtr> m_completed_sync;
// How many waiting items each RetrieveWorkItems compiles, deferred.
static constexpr size_t DEFERRED_ITEMS_PER_RETRIEVE = 2;
bool m_deferred = false;
};
} // namespace VideoCommon
+44 -11
View File
@@ -76,12 +76,16 @@ void ShaderCache::InitializeShaderCache()
* known, go on building in the background while it runs. Without
* threads to build on, the asynchronous modes draw as Synchronous does
* (VertexManagerBase::UpdatePipelineObject), and nothing more is built
* before the game starts than that would. */
* before the game starts than that would. Where the driver compiles on
* threads of its own instead, a few a frame (AsyncShaderCompiler
* deferred), every ubershader pipeline there can be is not built: each
* still costs the frontend's thread the start of its compile. */
if (g_ActiveConfig.UsingUberShaders() && CanCompileInBackground())
{
CompileMissingUberPipelines();
WaitForAsyncCompiler();
QueueUberShaderPipelines();
if (m_async_shader_compiler->HasWorkerThreads())
QueueUberShaderPipelines();
}
#else
// Queue ubershader precompiling if required.
@@ -100,7 +104,8 @@ void ShaderCache::InitializeShaderCache()
bool ShaderCache::CanCompileInBackground() const
{
return m_async_shader_compiler && m_async_shader_compiler->HasWorkerThreads();
return m_async_shader_compiler &&
(m_async_shader_compiler->HasWorkerThreads() || m_async_shader_compiler->IsDeferred());
}
void ShaderCache::Reload()
@@ -130,6 +135,22 @@ void ShaderCache::Reload()
void ShaderCache::RetrieveAsyncShaders()
{
m_async_shader_compiler->RetrieveWorkItems();
/* the cache data of those the driver has finished */
size_t i = 0;
while (i < m_unwritten_gx_pipelines.size())
{
auto it = m_gx_pipeline_cache.find(m_unwritten_gx_pipelines[i]);
if (it != m_gx_pipeline_cache.end() && it->second.first && it->second.first->IsBuilding())
{
i++;
continue;
}
if (it != m_gx_pipeline_cache.end() && it->second.first && it->second.first->IsReady())
WriteGXPipelineCacheData(it->first, it->second.first.get());
m_unwritten_gx_pipelines[i] = m_unwritten_gx_pipelines.back();
m_unwritten_gx_pipelines.pop_back();
}
}
void ShaderCache::Shutdown()
@@ -398,6 +419,7 @@ void ShaderCache::LoadCaches()
void ShaderCache::ClearCaches()
{
m_unwritten_gx_pipelines.clear();
ClearPipelineCache(m_gx_pipeline_cache, m_gx_pipeline_disk_cache);
ClearShaderCache(m_vs_cache);
ClearShaderCache(m_gs_cache);
@@ -761,20 +783,30 @@ const AbstractPipeline* ShaderCache::InsertGXPipeline(const GXPipelineUid& confi
if (g_ActiveConfig.bShaderCache)
{
auto cache_data = entry.first->GetCacheData();
if (!cache_data.empty())
{
SerializedGXPipelineUid disk_uid;
SerializePipelineUid(config, disk_uid);
m_gx_pipeline_disk_cache.Append(disk_uid, cache_data.data(),
static_cast<u32>(cache_data.size()));
}
/* its data is there to read once the driver is done with it */
if (entry.first->IsBuilding())
m_unwritten_gx_pipelines.push_back(config);
else
WriteGXPipelineCacheData(config, entry.first.get());
}
}
return entry.first.get();
}
void ShaderCache::WriteGXPipelineCacheData(const GXPipelineUid& config,
const AbstractPipeline* pipeline)
{
auto cache_data = pipeline->GetCacheData();
if (!cache_data.empty())
{
SerializedGXPipelineUid disk_uid;
SerializePipelineUid(config, disk_uid);
m_gx_pipeline_disk_cache.Append(disk_uid, cache_data.data(),
static_cast<u32>(cache_data.size()));
}
}
const AbstractPipeline*
ShaderCache::InsertGXUberPipeline(const GXUberPipelineUid& config,
std::unique_ptr<AbstractPipeline> pipeline)
@@ -952,6 +984,7 @@ void ShaderCache::LoadUberPipelineUIDCache()
* that adding is no allocation in the middle of a game */
m_pending_gx_uber_pipeline_uids.reserve(1024);
m_pending_gx_pipeline_uids.reserve(1024);
m_unwritten_gx_pipelines.reserve(256);
}
void ShaderCache::AppendGXUberPipelineUID(const GXUberPipelineUid& config)
+3
View File
@@ -163,6 +163,7 @@ private:
const BlendingState& blending_state, AbstractPipelineUsage usage);
std::optional<AbstractPipelineConfig> GetGXPipelineConfig(const GXPipelineUid& uid);
std::optional<AbstractPipelineConfig> GetGXPipelineConfig(const GXUberPipelineUid& uid);
void WriteGXPipelineCacheData(const GXPipelineUid& config, const AbstractPipeline* pipeline);
const AbstractPipeline* InsertGXPipeline(const GXPipelineUid& config,
std::unique_ptr<AbstractPipeline> pipeline);
const AbstractPipeline* InsertGXUberPipeline(const GXUberPipelineUid& config,
@@ -252,6 +253,8 @@ private:
std::vector<SerializedGXUberPipelineUid> m_pending_gx_uber_pipeline_uids;
/* those already in the file or written to it this session */
std::set<GXUberPipelineUid> m_recorded_uber_pipeline_uids;
// Pipelines whose cache data is written once their compile, on the driver's threads, is done.
std::vector<GXPipelineUid> m_unwritten_gx_pipelines;
#endif
Common::LinearDiskCache<SerializedGXPipelineUid, u8> m_gx_pipeline_disk_cache;
Common::LinearDiskCache<SerializedGXUberPipelineUid, u8> m_gx_uber_pipeline_disk_cache;
@@ -891,12 +891,18 @@ void VertexManagerBase::UpdatePipelineObject()
#endif
// Can we background compile shaders? If so, get the pipeline asynchronously.
auto res = g_shader_cache->GetPipelineForUidAsync(m_current_pipeline_config);
if (res)
if (res && (!*res || (*res)->IsReady()))
{
// Specialized shaders are ready, prefer these.
m_current_pipeline_object = *res;
return;
}
#ifdef __LIBRETRO__
/* built and still compiling on the driver's threads: asked again
* next draw, until it is ready */
if (res)
m_pipeline_config_changed = true;
#endif
if (g_ActiveConfig.iShaderCompilationMode == ShaderCompilationMode::AsynchronousUberShaders)
{