gpu, scripts: resolve startup stalls via async pipeline warmup and cached pre-launch

Eliminate multi-minute boot delays across pre-window launch and initial black screen presentation:

- Vulkan (vk_pipeline_serialization): Dispatch cached graphics pipelines asynchronously to worker threads via pending_graphics during WarmUp, removing the synchronous driver compilation stall on the render thread before the first frame.
- Vulkan (vk_shader_rebuild): Protect RebuildDirectory on Windows with Vectored Exception Handling to safely bypass incompatible shaders, and persist ShaderBinaryVersion in profile.bin on failure to stop repeating broken rebuilds every launch.
- Vulkan (vk_pipeline_import): Cap startup shader recipe merges to a 5 ms budget, deferring remaining imports to per-frame polls.
- Host Runtime (probe.c): Defer bbgpu_rebuild_shader_caches() until after bbgpu_init() so the SDL window and presenter open immediately.
- Pre-launch Scripts (prepare, link_libc, link_modules): Add mtime checks against source binaries to reuse existing boot images, and remove the redundant 28,000-file traversal in prepare.py.
- Platform (nvidia_profile): Add a single-run guard to BbNvidiaProfile::Apply() to prevent redundant NVAPI driver queries.
This commit is contained in:
Mrsuss60 committed 2026-11-02 17:47:31 +00:00
1 parent 2054129fae
commit ed73550c85
8 files changed
+168 -31

No files matched your search

+3 -1
View File
@@ -125,9 +125,11 @@ struct Api {
} // namespace
void Apply() {
if (bb_env_off("BB_NV_POWER")) {
static bool applied = false;
if (applied || bb_env_off("BB_NV_POWER")) {
return;
}
applied = true;
Api api{};
if (!api.Load() || api.initialize() != Ok) {
return; // no NVIDIA driver
@@ -333,9 +333,9 @@ void PipelineCache::MergeShaderImport(bool at_start) {
return;
}
state.frame = frame;
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(2);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(at_start ? 5 : 2);
const auto in_budget = [&] {
return at_start || std::chrono::steady_clock::now() < deadline;
return std::chrono::steady_clock::now() < deadline;
};
while (state.next_program < state.programs.size() && in_budget()) {
MergeImportedProgram(state, state.next_program++);
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/serdes.h"
#include "common/scope_exit.h"
#include "core/emulator_settings.h"
#include "shader_recompiler/frontend/fetch_shader.h"
#include "shader_recompiler/info.h"
@@ -705,27 +706,76 @@ void PipelineCache::WarmUp() {
}
const std::array<Shader::RuntimeInfo, MaxShaderStages> empty_runtime_infos{};
for (auto& entry : staged_graphics) {
bool all_stages_ok = true;
std::array<vk::ShaderModule, MaxShaderStages> modules{};
for (size_t s = 0; s < MaxShaderStages; ++s) {
if (entry.futures[s].valid()) {
modules[s] = entry.futures[s].get();
if (!modules[s]) {
all_stages_ok = false;
break;
if (warmup_async) {
for (auto& entry : staged_graphics) {
if (graphics_pipelines.find(entry.key) != graphics_pipelines.end() ||
pending_graphics.find(entry.key) != pending_graphics.end()) {
continue;
}
auto job = std::make_shared<PendingPipeline>();
job->saved = true;
job->sdata = entry.sdata;
job->make = [this, key = entry.key, infos = entry.infos,
fetch = entry.fetch_shader, futures = entry.futures,
sdata = entry.sdata]() mutable -> std::unique_ptr<GraphicsPipeline> {
std::array<vk::ShaderModule, MaxShaderStages> modules{};
for (size_t s = 0; s < MaxShaderStages; ++s) {
if (futures[s].valid()) {
modules[s] = futures[s].get();
if (!modules[s]) {
return nullptr;
}
}
}
const std::array<Shader::RuntimeInfo, MaxShaderStages> empty_runtime_infos{};
return std::make_unique<GraphicsPipeline>(
instance, scheduler, desc_heap, profile, key, *pipeline_cache,
infos, empty_runtime_infos, fetch, modules, sdata, true);
};
pending_graphics.emplace(entry.key, job);
++num_pipelines;
g_bb_background_pipelines_total.fetch_add(1, std::memory_order_relaxed);
GetCompiler().Submit([job] {
SCOPE_EXIT {
g_bb_background_pipelines_done.fetch_add(1, std::memory_order_relaxed);
};
if (job->started.exchange(true)) {
return;
}
std::unique_ptr<GraphicsPipeline> pipeline;
try {
pipeline = job->make();
} catch (const std::exception&) {
}
std::unique_lock lk{job->mutex};
job->pipeline = std::move(pipeline);
job->done = true;
job->cv.notify_all();
});
}
} else {
for (auto& entry : staged_graphics) {
bool all_stages_ok = true;
std::array<vk::ShaderModule, MaxShaderStages> modules{};
for (size_t s = 0; s < MaxShaderStages; ++s) {
if (entry.futures[s].valid()) {
modules[s] = entry.futures[s].get();
if (!modules[s]) {
all_stages_ok = false;
break;
}
}
}
}
if (!all_stages_ok) {
continue;
}
const auto [it, is_new] = graphics_pipelines.try_emplace(entry.key);
if (is_new) {
it.value() = std::make_unique<GraphicsPipeline>(
instance, scheduler, desc_heap, profile, entry.key, *pipeline_cache,
entry.infos, empty_runtime_infos, entry.fetch_shader, modules, entry.sdata, true);
++num_pipelines;
if (!all_stages_ok) {
continue;
}
const auto [it, is_new] = graphics_pipelines.try_emplace(entry.key);
if (is_new) {
it.value() = std::make_unique<GraphicsPipeline>(
instance, scheduler, desc_heap, profile, entry.key, *pipeline_cache,
entry.infos, empty_runtime_infos, entry.fetch_shader, modules, entry.sdata, true);
++num_pipelines;
}
}
}
}
@@ -173,6 +173,12 @@ std::vector<u8> RewriteMeta(std::vector<u8>&& meta, const std::vector<u8>& info,
bool RebuildDirectory(const fs::path& dir, const Shader::Profile& profile, u32& translated,
u32& failed) {
#ifdef _WIN32
void (*old_abrt)(int) = std::signal(SIGABRT, RecipeSignalHandler);
void (*old_segv)(int) = std::signal(SIGSEGV, RecipeSignalHandler);
void (*old_ill)(int) = std::signal(SIGILL, RecipeSignalHandler);
PVOID veh = AddVectoredExceptionHandler(1, RecipeVectoredHandler);
#endif
struct Record {
fs::path path;
BbShaderCapture::Input input;
@@ -196,11 +202,23 @@ bool RebuildDirectory(const fs::path& dir, const Shader::Profile& profile, u32&
std::vector<u8> program_info;
for (auto& [perm_idx, record] : perms) {
std::optional<Translated> result;
#ifdef _WIN32
if (setjmp(g_recipe_jmp) == 0) {
try {
result = Translate(record.input, profile);
} catch (const std::exception&) {
result.reset();
}
} else {
result.reset();
}
#else
try {
result = Translate(record.input, profile);
} catch (const std::exception&) {
result.reset();
}
#endif
const std::string name = HexName(pgm_hash) + "_" + std::to_string(perm_idx);
if (!result) {
fs::remove(dir / (name + ".spv"), ec);
@@ -232,6 +250,14 @@ bool RebuildDirectory(const fs::path& dir, const Shader::Profile& profile, u32&
++translated;
}
}
#ifdef _WIN32
std::signal(SIGABRT, old_abrt);
std::signal(SIGSEGV, old_segv);
std::signal(SIGILL, old_ill);
if (veh) {
RemoveVectoredExceptionHandler(veh);
}
#endif
return true;
}
@@ -576,6 +602,9 @@ int RebuildCaches() {
if (!rebuilt_ok) {
std::printf("Shader cache %s: rebuilding failed, it is compiled again in the game\n",
dir.filename().string().c_str());
versions[1] = Vulkan::PipelineStore::ShaderBinaryVersion;
std::memcpy(header.data(), versions, sizeof(versions));
WriteFile(dir / "profile.bin", header);
continue;
}
versions[1] = Vulkan::PipelineStore::ShaderBinaryVersion;
+20
View File
@@ -74,6 +74,26 @@ def module(path):
def link(game, out):
main_eboot = game / 'eboot.bin' if (game / 'eboot.bin').is_file() else game / 'eboot.elf'
boot_libc = out / 'boot-libc.bin'
libc_json = out / 'libc-link.json'
boot_bin = out / 'boot.bin'
libc_prx = game / 'sce_module/libc.prx'
if boot_libc.is_file() and libc_json.is_file() and boot_bin.is_file() and libc_prx.is_file():
m = boot_libc.stat().st_mtime
if m >= boot_bin.stat().st_mtime and m >= libc_prx.stat().st_mtime and m >= Path(__file__).stat().st_mtime:
try:
rep = json.loads(libc_json.read_text(encoding='utf-8', errors='replace'))
base = int(rep['base'], 16)
bindings = rep.get('bindings', 0)
tls_memsz = rep.get('tls_memory_bytes', 0)
main_tls = rep.get('main_tls', {})
eboot_tls = main_tls.get('memsz', 0)
patched = rep.get('fs_loads_patched', 0)
print(f'Linked native libc: base={base:#x}, {bindings} fallback exports, TLS={tls_memsz} bytes; '
f'eboot TLS={eboot_tls} bytes, fs->gs patched={patched}')
return
except Exception:
pass
main = module(main_eboot)
libc = module(game/'sce_module/libc.prx')
raw = (out/'boot.bin').read_bytes()
+18
View File
@@ -94,6 +94,24 @@ def patch_fs_loads(image, ph, base):
def link(game, out, module_names=DEFAULT_MODULES):
main_eboot = game / 'eboot.bin' if (game / 'eboot.bin').is_file() else game / 'eboot.elf'
boot_linked = out / 'boot-linked.bin'
link_json = out / 'link.json'
boot_bin = out / 'boot.bin'
if boot_linked.is_file() and link_json.is_file() and boot_bin.is_file():
m = boot_linked.stat().st_mtime
if m >= boot_bin.stat().st_mtime and m >= Path(__file__).stat().st_mtime:
try:
rep = json.loads(link_json.read_text(encoding='utf-8', errors='replace'))
table = rep.get('modules', [])
summary = ', '.join(f"{t.get('file', 'mod')}@{t.get('base', '0')}" for t in table)
bindings = rep.get('bindings', 0)
unresolved = len(rep.get('unresolved_imports', []))
fs_patched = rep.get('fs_loads_patched', 0)
print(f'Linked modules: {summary}; {bindings} native bindings, {unresolved} imports left to the host runtime, '
f'fs->gs patched={fs_patched}')
return
except Exception:
pass
main = module(main_eboot)
raw = (out / 'boot.bin').read_bytes()
magic, size, entry, ns, nr, ni, flags = unpack('<8s6Q', raw, 0)
+25 -6
View File
@@ -11,6 +11,7 @@ import hashlib
import json
from pathlib import Path
import struct
import sys
def span(data, offset, size):
@@ -147,6 +148,26 @@ def prepare(game, out):
eboot_path = find_eboot(game)
if not eboot_path:
raise FileNotFoundError(f"Missing eboot.bin or eboot.elf in '{game}'. Please check your game directory.")
boot_bin = out / 'boot.bin'
analysis_json = out / 'analysis.json'
eboot_mtime = eboot_path.stat().st_mtime
script_mtime = Path(__file__).stat().st_mtime
if boot_bin.is_file() and analysis_json.is_file():
boot_mtime = boot_bin.stat().st_mtime
if boot_mtime >= eboot_mtime and boot_mtime >= script_mtime:
try:
report = json.loads(analysis_json.read_text(encoding='utf-8', errors='replace'))
title = report['sfo'].get('TITLE', 'Bloodborne')
safe_title = title.encode(sys.stdout.encoding or 'utf-8', 'replace').decode(sys.stdout.encoding or 'utf-8', 'replace')
print(f"{safe_title} | entry={report.get('entry')} | image={report.get('image_bytes', 0):,} bytes")
print(f"{report.get('import_count', len(report.get('imports', [])))} imported symbols; {sum(report.get('relocation_counts', {}).values()):,} relocations; {len(report.get('needed', []))} required modules")
print(f"Unavailable non-loadable metadata headers: {report.get('unavailable_metadata_headers', [])}; not a byte-exact ELF reconstruction")
print(f"Output: {out.resolve()}")
libc_evidence = report.get('libc_evidence', {})
print(f"libc _init_env verified RET: {libc_evidence.get('init_env_is_ret', True)}")
return
except Exception:
pass
source = eboot_path.read_bytes()
if len(source) == 0:
raise ValueError(f"{eboot_path.name} at '{eboot_path}' is completely empty (0 bytes). Check your game dump or extraction.")
@@ -253,10 +274,6 @@ def prepare(game, out):
(out / 'entry.bin').write_bytes(image[:1024])
resources = collections.Counter()
total_bytes = 0
for path in (game / 'dvdroot_ps4').rglob('*'):
if path.is_file():
resources[path.relative_to(game / 'dvdroot_ps4').parts[0]] += 1
total_bytes += path.stat().st_size
report = dict(source_sha256=hashlib.sha256(source).hexdigest(), source_bytes=len(source),
sfo=sfo((game / 'sce_sys/param.sfo').read_bytes()), entry=hex(header[4]),
image_bytes=size, program_headers=ph, self_segments=segments,
@@ -267,8 +284,10 @@ def prepare(game, out):
bundled_modules=sorted(p.name for p in (game / 'sce_module').iterdir()),
resources=dict(resources), resource_bytes=total_bytes,
status='Prepared only; execution and Vulkan are tested separately.')
(out / 'analysis.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
print(f"{report['sfo'].get('TITLE')} | entry={header[4]:#x} | image={size:,} bytes")
(out / 'analysis.json').write_text(json.dumps(report, ensure_ascii=True, indent=2) + '\n', encoding='utf-8')
title = report['sfo'].get('TITLE', 'Bloodborne')
safe_title = title.encode(sys.stdout.encoding or 'utf-8', 'replace').decode(sys.stdout.encoding or 'utf-8', 'replace')
print(f"{safe_title} | entry={header[4]:#x} | image={size:,} bytes")
print(f"{len(names)} imported symbols; {sum(counts.values()):,} relocations; {len(report['needed'])} required modules")
print(f"Unavailable non-loadable metadata headers: {missing}; not a byte-exact ELF reconstruction")
print(f"Output: {out.resolve()}")
+2 -3
View File
@@ -2963,9 +2963,6 @@ int main(int argc, char **argv) {
if (argc == 9 && !strcmp(argv[1], "--merge-shader-metas"))
return bbgpu_merge_shader_metas(argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], atoi(argv[8]));
/* Rebuild caches from older compiler versions if needed */
bbgpu_rebuild_shader_caches();
#ifdef _WIN32
runtime_memory_reserve_space();
int avx_status = runtime_win_check_avx();
@@ -3227,6 +3224,8 @@ int main(int argc, char **argv) {
gpu_enabled=1; /* page tracking starts while the rasterizer registers guest memory */
if (bbgpu_init(&gpu)) fail("GPU initialization failed");
printf("GPU: window and Vulkan presenter ready; SDK 0x%08x, %u HLE symbols\n",(unsigned)sdk,bbgpu_symbol_count());
/* Rebuild caches from older compiler versions if needed */
bbgpu_rebuild_shader_caches();
}
image_size = size;
resolved_targets = calloc(import_count + 1, sizeof(void *));