mirror of
https://github.com/Mrsuss60/bloodborne_pc_windows_port.git
synced 2026-11-07 05:56:20 -10:00
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:
1 parent
2054129fae
commit
ed73550c85
8 files changed
+168
-31
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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 *));
|
||||
|
||||
Reference in new issue
Block a user