Shaders: ubershaders by default, seeded per game

Shader compilation defaults to Async (UberShaders): a draw whose own
pipeline is not built yet is drawn with an ubershader one, exact
rendering, while its own builds on the compiler threads, which now
number as many as the machine has to spare (cores - 3, 1 to 4) instead
of one.

What is left to stutter is an ubershader pipeline itself, built on the
spot the first time a draw needs one: one per vertex layout and fixed
function state. Every ubershader pipeline a draw uses is now recorded,
in <game id>.uberuidcache next to the specialized UID cache, and those
are built before the game starts the next time it boots; every other
ubershader pipeline there can be, and the specialized pipelines known,
build in the background while it runs instead of before it starts,
which took 21 s on lavapipe. The record is written when the cache
closes, into room reserved when it opens, so recording allocates
nothing in the middle of a game.

Without compiler threads (OpenGL here, with no shared context for
them), asking for a pipeline in the background builds it on the spot
and an ubershader one besides: the asynchronous modes draw as
Synchronous does then, and nothing extra is built before the game
starts.

Booting a FIFO log no longer hangs: the CPU thread clears the EFB, which
waits on the GPU, before it reaches its run loop, and the frontend's
thread waited for that run loop without running the GPU. It runs the
GPU until then.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-11-06 23:07:17 +01:00
1 parent 58542ddb3a
commit e4bac5f75c
8 files changed
+163 -26

No files matched your search

-5
View File
@@ -39,7 +39,6 @@ CPUManager::CPUManager(Core::System& system) : m_system(system)
retro_eventcount_init(&m_cpu_idle);
#ifdef __LIBRETRO__
retro_atomic_int_init(&m_cpu_run_state_reached, 0);
retro_eventcount_init(&m_run_state_event);
#endif
}
@@ -51,9 +50,6 @@ CPUManager::~CPUManager()
node = node->next;
delete job;
}
#ifdef __LIBRETRO__
retro_eventcount_free(&m_run_state_event);
#endif
retro_eventcount_free(&m_cpu_idle);
retro_eventcount_free(&m_cpu_wake);
retro_eventcount_free(&m_stepping_released);
@@ -162,7 +158,6 @@ void CPUManager::Run()
#ifdef __LIBRETRO__
retro_atomic_store_release_int(&m_cpu_run_state_reached, 1);
retro_eventcount_notify(&m_run_state_event);
#endif
while (LoadState() != State::PowerDown)
{
-15
View File
@@ -113,20 +113,6 @@ public:
{
return retro_atomic_load_acquire_int(&m_cpu_run_state_reached) != 0;
}
// Blocks until the CPU thread has entered its run loop.
void WaitForCPURunState()
{
for (;;)
{
const int key = retro_eventcount_prepare_wait(&m_run_state_event);
if (HasCPURunStateBeenReached())
{
retro_eventcount_cancel_wait(&m_run_state_event);
return;
}
retro_eventcount_commit_wait(&m_run_state_event, key);
}
}
#endif
private:
struct Job
@@ -179,7 +165,6 @@ private:
Core::System& m_system;
#ifdef __LIBRETRO__
retro_atomic_int_t m_cpu_run_state_reached;
retro_eventcount_t m_run_state_event;
#endif
};
} // namespace CPU
+5 -1
View File
@@ -508,7 +508,11 @@ bool retro_load_game(const struct retro_game_info* game)
static_cast<ShaderCompilationMode>(
Libretro::GetOption<int>(
gfx_settings::SHADER_COMPILATION_MODE,
static_cast<int>(ShaderCompilationMode::Synchronous))));
static_cast<int>(ShaderCompilationMode::AsynchronousUberShaders))));
/* as many compiler threads as the machine has to spare, for building
* pipelines both before the game starts and while it runs */
Config::SetBase(Config::GFX_SHADER_COMPILER_THREADS, -1);
Config::SetBase(Config::GFX_SHADER_PRECOMPILER_THREADS, -1);
Config::SetBase(Config::GFX_WAIT_FOR_SHADERS_BEFORE_STARTING,
Libretro::GetOption<bool>(gfx_settings::WAIT_FOR_SHADERS, /*def=*/false));
@@ -863,17 +863,20 @@ static struct retro_core_option_v2_definition option_defs[] = {
Libretro::Options::gfx_settings::SHADER_COMPILATION_MODE,
"Graphics > Settings > Shader Compilation",
"Shader Compilation",
"Control shader compilation.",
"Control shader compilation. Async (UberShaders) draws with ubershaders, built before the "
"game starts, while each draw's own pipeline builds in the background: exact rendering "
"without compilation stutter, at some GPU cost while new pipelines build. Synchronous "
"builds each new pipeline when it is first drawn, stuttering.",
nullptr,
CATEGORY_GFX_SETTINGS,
{
{ "2", "Async (UberShaders)" }, // ShaderCompilationMode::AsynchronousUberShaders
{ "0", "Synchronous" }, // ShaderCompilationMode::Synchronous
{ "3", "Async (Skip Rendering)" }, // ShaderCompilationMode::AsynchronousSkipRendering
{ "1", "Sync (UberShaders)" }, // ShaderCompilationMode::SynchronousUberShaders
{ "2", "Async (UberShaders)" }, // ShaderCompilationMode::AsynchronousUberShaders
{ nullptr, nullptr }
},
"0"
"2"
},
{
Libretro::Options::gfx_settings::WAIT_FOR_SHADERS,
+7 -2
View File
@@ -333,8 +333,13 @@ void retro_run(void)
{
Core::StartEmuThread(system, std::move(Core::g_boot_params), wsi);
// Wait until CPU thread has reached Run()
system.GetCPU().WaitForCPURunState();
/* until the CPU thread has reached its run loop, this thread runs
* the GPU: what the CPU thread does first may wait on it, as the
* FIFO player clearing the EFB does */
Libretro::Video::D3D11Lock();
while (!system.GetCPU().HasCPURunStateBeenReached())
system.GetFifo().RunGpuLoop(5000);
Libretro::Video::D3D11Unlock();
}
else
Core::EmuThread(system, std::move(Core::g_boot_params), wsi);
+118
View File
@@ -62,11 +62,32 @@ void ShaderCache::InitializeShaderCache()
{
LoadCaches();
LoadPipelineUIDCache();
#ifdef __LIBRETRO__
if (g_ActiveConfig.UsingUberShaders())
LoadUberPipelineUIDCache();
#endif
}
#ifdef __LIBRETRO__
/* With ubershaders, every draw without its specialized pipeline yet is
* drawn with an ubershader one. The ones this game drew with before are
* built before it starts, as a draw would otherwise build its own on
* the spot; every other one there can be, and the specialized ones
* 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. */
if (g_ActiveConfig.UsingUberShaders() && CanCompileInBackground())
{
CompileMissingUberPipelines();
WaitForAsyncCompiler();
QueueUberShaderPipelines();
}
#else
// Queue ubershader precompiling if required.
if (g_ActiveConfig.UsingUberShaders())
QueueUberShaderPipelines();
#endif
// Compile all known UIDs.
CompileMissingPipelines();
@@ -77,6 +98,11 @@ void ShaderCache::InitializeShaderCache()
m_async_shader_compiler->ResizeWorkerThreads(g_ActiveConfig.GetShaderCompilerThreads());
}
bool ShaderCache::CanCompileInBackground() const
{
return m_async_shader_compiler && m_async_shader_compiler->HasWorkerThreads();
}
void ShaderCache::Reload()
{
WaitForAsyncCompiler();
@@ -155,6 +181,12 @@ std::optional<const AbstractPipeline*> ShaderCache::GetPipelineForUidAsync(const
const AbstractPipeline* ShaderCache::GetUberPipelineForUid(const GXUberPipelineUid& uid)
{
auto it = m_gx_uber_pipeline_cache.find(uid);
#ifdef __LIBRETRO__
/* every one a draw uses is recorded, to be built before the game
* starts next time */
if (m_gx_uber_pipeline_uid_cache_file.IsOpen() && !m_recorded_uber_pipeline_uids.contains(uid))
AppendGXUberPipelineUID(uid);
#endif
if (it != m_gx_uber_pipeline_cache.end() && !it->second.second)
return it->second.first.get();
@@ -853,10 +885,96 @@ void ShaderCache::ClosePipelineUIDCache()
WARN_LOG_FMT(VIDEO, "Writing pipeline UIDs to cache failed.");
}
m_pending_gx_pipeline_uids.clear();
if (m_gx_uber_pipeline_uid_cache_file.IsOpen() && !m_pending_gx_uber_pipeline_uids.empty() &&
!m_gx_uber_pipeline_uid_cache_file.WriteArray(m_pending_gx_uber_pipeline_uids.data(),
m_pending_gx_uber_pipeline_uids.size()))
{
WARN_LOG_FMT(VIDEO, "Writing ubershader pipeline UIDs to cache failed.");
}
m_pending_gx_uber_pipeline_uids.clear();
m_recorded_uber_pipeline_uids.clear();
m_gx_uber_pipeline_uid_cache_file.Close();
#endif
m_gx_pipeline_uid_cache_file.Close();
}
#ifdef __LIBRETRO__
void ShaderCache::LoadUberPipelineUIDCache()
{
const u32 magic = 0x44495555; // UUID
const u32 header_size = sizeof(u32) + sizeof(u32);
const std::string filename =
File::GetUserPath(D_CACHE_IDX) + SConfig::GetInstance().GetGameID() + ".uberuidcache";
File::IOFile& file = m_gx_uber_pipeline_uid_cache_file;
bool valid = false;
if (file.Open(filename, "rb+"))
{
u32 existing_magic;
u32 existing_version;
if (file.ReadBytes(&existing_magic, sizeof(existing_magic)) &&
file.ReadBytes(&existing_version, sizeof(existing_version)) && existing_magic == magic &&
existing_version == GX_PIPELINE_UID_VERSION)
{
const u64 file_size = file.GetSize();
const u64 count = (file_size - header_size) / sizeof(SerializedGXUberPipelineUid);
u64 i;
valid = file_size == count * sizeof(SerializedGXUberPipelineUid) + header_size;
for (i = 0; valid && i < count; i++)
{
SerializedGXUberPipelineUid serialized_uid;
GXUberPipelineUid real_uid;
if (!file.ReadBytes(&serialized_uid, sizeof(serialized_uid)))
{
valid = false;
break;
}
UnserializePipelineUid(serialized_uid, real_uid);
/* known, not built: built with the ubershaders before the game
* starts */
if (!m_gx_uber_pipeline_cache.contains(real_uid))
m_gx_uber_pipeline_cache[real_uid].second = false;
m_recorded_uber_pipeline_uids.insert(real_uid);
}
if (valid)
valid = file.Seek(file_size, File::SeekOrigin::Begin);
}
if (!valid)
file.Close();
}
if (!file.IsOpen() && file.Open(filename, "wb"))
{
file.WriteBytes(&magic, sizeof(magic));
file.WriteBytes(&GX_PIPELINE_UID_VERSION, sizeof(GX_PIPELINE_UID_VERSION));
}
/* what a session adds is written when the cache closes: room enough
* 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);
}
void ShaderCache::AppendGXUberPipelineUID(const GXUberPipelineUid& config)
{
SerializedGXUberPipelineUid disk_uid;
if (!m_gx_uber_pipeline_uid_cache_file.IsOpen())
return;
SerializePipelineUid(config, disk_uid);
m_pending_gx_uber_pipeline_uids.push_back(disk_uid);
m_recorded_uber_pipeline_uids.insert(config);
}
void ShaderCache::CompileMissingUberPipelines()
{
for (auto& it : m_gx_uber_pipeline_cache)
{
if (!it.second.first && !it.second.second)
QueueUberPipelineCompile(it.first, COMPILE_PRIORITY_UBERSHADER_PIPELINE);
}
}
#endif
void ShaderCache::AddSerializedGXPipelineUID(const SerializedGXPipelineUid& uid)
{
GXPipelineUid real_uid;
+18
View File
@@ -7,6 +7,7 @@
#include <cstddef>
#include <cstring>
#include <map>
#include <set>
#include <memory>
#include <optional>
#include <string>
@@ -46,6 +47,10 @@ namespace VideoCommon
class ShaderCache final
{
public:
/* Whether pipelines can build on threads of their own: without, asking
* for one in the background builds it on the spot all the same. */
bool CanCompileInBackground() const;
ShaderCache();
~ShaderCache();
@@ -164,6 +169,12 @@ private:
std::unique_ptr<AbstractPipeline> pipeline);
void AddSerializedGXPipelineUID(const SerializedGXPipelineUid& uid);
void AppendGXPipelineUID(const GXPipelineUid& config);
#ifdef __LIBRETRO__
void LoadUberPipelineUIDCache();
void AppendGXUberPipelineUID(const GXUberPipelineUid& config);
/* queues the ubershader pipelines known and not built yet */
void CompileMissingUberPipelines();
#endif
// ASync Compiler Methods
void QueueVertexShaderCompile(const VertexShaderUid& uid, u32 priority);
@@ -234,6 +245,13 @@ private:
File::IOFile m_gx_pipeline_uid_cache_file;
#ifdef __LIBRETRO__
std::vector<SerializedGXPipelineUid> m_pending_gx_pipeline_uids;
/* The ubershader pipelines a game drew with, as for the specialized
* ones: compiled while the game boots next time, so a hybrid draw
* finds its ubershader ready instead of building it on the spot. */
File::IOFile m_gx_uber_pipeline_uid_cache_file;
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;
#endif
Common::LinearDiskCache<SerializedGXPipelineUid, u8> m_gx_pipeline_disk_cache;
Common::LinearDiskCache<SerializedGXUberPipelineUid, u8> m_gx_uber_pipeline_disk_cache;
@@ -880,6 +880,15 @@ void VertexManagerBase::UpdatePipelineObject()
case ShaderCompilationMode::AsynchronousUberShaders:
case ShaderCompilationMode::AsynchronousSkipRendering:
{
#ifdef __LIBRETRO__
/* no threads to build on: asking for the pipeline in the background
* would build it on the spot, then an ubershader one besides */
if (!g_shader_cache->CanCompileInBackground())
{
m_current_pipeline_object = g_shader_cache->GetPipelineForUid(m_current_pipeline_config);
return;
}
#endif
// Can we background compile shaders? If so, get the pipeline asynchronously.
auto res = g_shader_cache->GetPipelineForUidAsync(m_current_pipeline_config);
if (res)