mirror of
https://github.com/Awakenchan/LanternEngine.git
synced 2026-11-10 09:21:42 -10:00
182 lines
7.2 KiB
Python
182 lines
7.2 KiB
Python
"""The GPU cache before a start, as bb-start.exe keeps it on Windows (start/main.cpp).
|
|
|
|
When the shader compiler changed (out/gpu-cache.id), each cache folder is recompiled by bb-shader-lab
|
|
from the shader states kept beside its shaders, so its pipelines are built at the start instead of
|
|
in play; a folder it cannot recompile is removed. Then each shader catalog shipped in
|
|
engine/shader-catalog is seeded once into the cache (its pipelines built before play too).
|
|
BB_SHADER_STATES=0 / BB_SHADER_CATALOG=0: not done.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
def log(text):
|
|
print(text, flush=True)
|
|
|
|
|
|
def lab_run(lab, *args):
|
|
"""bb-shader-lab's exit code and output (its log lines on stderr pass through)."""
|
|
env = dict(os.environ)
|
|
env['LD_LIBRARY_PATH'] = os.pathsep.join(p for p in (str(lab.parent / 'gpu'), env.get('LD_LIBRARY_PATH', '')) if p)
|
|
try:
|
|
done = subprocess.run([str(lab), *map(str, args)], stdout=subprocess.PIPE, text=True,
|
|
errors='replace', env=env)
|
|
except OSError as error:
|
|
return -1, str(error)
|
|
return done.returncode, done.stdout
|
|
|
|
|
|
def line_after(output, prefix):
|
|
at = output.rfind(prefix)
|
|
if at < 0:
|
|
return None
|
|
return output[at + len(prefix):].split('\n', 1)[0].strip()
|
|
|
|
|
|
def recompile(cache, lab):
|
|
"""Every cache folder recompiled for this engine; False when one could not be (removed)."""
|
|
if os.environ.get('BB_SHADER_STATES', '1') == '0' or not lab.is_file():
|
|
return False
|
|
ok = True
|
|
for folder in sorted(p for p in cache.iterdir() if p.is_dir() and (p / 'profile.bin').is_file()):
|
|
code, output = lab_run(lab, '--recompile-cache', folder)
|
|
summary = line_after(output, 'Shader cache:')
|
|
if code == 0 and summary is not None:
|
|
log(f'GPU cache {folder.name}: {summary}')
|
|
else:
|
|
shutil.rmtree(folder, ignore_errors=True)
|
|
log(f'GPU cache {folder.name}: not recompiled (exit {code}); its shaders compile again in play')
|
|
ok = False
|
|
return ok
|
|
|
|
|
|
def reset_stale(cache, identity, legacy, lab):
|
|
stamp = cache / 'engine.txt'
|
|
try:
|
|
stamped = stamp.read_text().strip()
|
|
except OSError:
|
|
stamped = None
|
|
if stamped is not None:
|
|
if not identity or stamped == identity:
|
|
return
|
|
if legacy and stamped == legacy:
|
|
stamp.write_text(identity + '\n')
|
|
return
|
|
if cache.is_dir() and recompile(cache, lab):
|
|
stamp.write_text(identity + '\n')
|
|
log(f'GPU cache: the shader compiler changed ({identity}); recompiled before the start')
|
|
return
|
|
removed = False
|
|
if cache.is_dir():
|
|
for entry in cache.iterdir():
|
|
if entry == stamp or entry.suffix.lower() == '.vkcache':
|
|
continue
|
|
if entry.is_dir() and not entry.is_symlink():
|
|
shutil.rmtree(entry, ignore_errors=True)
|
|
else:
|
|
entry.unlink(missing_ok=True)
|
|
removed = True
|
|
cache.mkdir(parents=True, exist_ok=True)
|
|
stamp.write_text(identity + '\n')
|
|
if removed:
|
|
log(f'GPU cache: cleared ({identity}: the shader compiler changed); shaders compile again on this start')
|
|
|
|
|
|
def gpu_vendors():
|
|
"""The PCI vendors of this PC's display adapters (Linux: /sys/class/drm)."""
|
|
vendors = set()
|
|
for card in Path('/sys/class/drm').glob('card*/device/vendor'):
|
|
try:
|
|
vendors.add(int(card.read_text().strip(), 16))
|
|
except (OSError, ValueError):
|
|
pass
|
|
return vendors
|
|
|
|
|
|
def catalog_vendor(catalog):
|
|
try:
|
|
words = (catalog / 'gpu.txt').read_text().split()
|
|
return int(words[1], 16) if words[0] == 'vendor' else 0
|
|
except (OSError, IndexError, ValueError):
|
|
return 0
|
|
|
|
|
|
def cache_vendor(folder):
|
|
"""The vendor of the GPU the engine last ran a cache on: its driver cache beside the folder
|
|
("BPBC", the vendor at 8); 0 before the first start."""
|
|
try:
|
|
with open(str(folder) + '.vkcache', 'rb') as f:
|
|
header = f.read(12)
|
|
except OSError:
|
|
return 0
|
|
return int.from_bytes(header[8:12], 'little') if len(header) == 12 and header[:4] == b'BPBC' else 0
|
|
|
|
|
|
def seed(cache, catalogs, game, eboot, lab):
|
|
if os.environ.get('BB_SHADER_CATALOG', '1') == '0' or not lab.is_file() or not catalogs.is_dir():
|
|
return
|
|
by_game = {}
|
|
for entry in sorted(p for p in catalogs.iterdir() if p.is_dir()):
|
|
if (entry / 'profile.bin').is_file():
|
|
by_game.setdefault(entry.name, []).append(entry)
|
|
continue
|
|
for sub in sorted(p for p in entry.iterdir() if p.is_dir() and (p / 'profile.bin').is_file()):
|
|
by_game.setdefault(sub.name, []).append(sub)
|
|
present = gpu_vendors()
|
|
for name, candidates in sorted(by_game.items()):
|
|
target = cache / name
|
|
vendor = cache_vendor(target) or next((v for v in (0x10DE, 0x1002, 0x8086) if v in present), 0)
|
|
catalog = next((c for c in candidates if vendor and catalog_vendor(c) == vendor), None)
|
|
if catalog is None:
|
|
continue
|
|
if not target.exists():
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(catalog / 'profile.bin', target / 'profile.bin')
|
|
log(f"GPU cache {catalog.name}: first start; the shader catalog's GPU profile used until the engine checks it")
|
|
if not (target / 'profile.bin').is_file():
|
|
continue
|
|
files = [p for p in catalog.iterdir()]
|
|
marker = target / f'catalog-{len(files)}-{sum(p.stat().st_size for p in files if p.is_file())}.applied'
|
|
if marker.exists():
|
|
continue
|
|
code, output = lab_run(lab, '--seed-cache', catalog, target, game, eboot)
|
|
first = line_after(output, 'Shader catalog:')
|
|
if first is not None:
|
|
log(f'GPU cache {catalog.name}: {first}')
|
|
last = line_after(output, 'Shader cache:')
|
|
if code == 0 and last is not None:
|
|
log(f'GPU cache {catalog.name}: {last}')
|
|
marker.write_text(f'{code}\n')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
p = argparse.ArgumentParser(description=__doc__)
|
|
p.add_argument('--cache', type=Path, required=True, help='BB_GPU_USER_DIR/cache')
|
|
p.add_argument('--engine', type=Path, required=True, help='the engine folder (shader-catalog)')
|
|
p.add_argument('--out', type=Path, required=True, help='engine/out: bb-shader-lab, gpu-cache.id')
|
|
p.add_argument('--package', type=Path, help='package.json (the engine id when no gpu-cache.id)')
|
|
p.add_argument('--game', type=Path, help='the game folder: the shader catalogs are seeded too (out/eboot.elf)')
|
|
a = p.parse_args()
|
|
lab = a.out / 'bb-shader-lab'
|
|
identity, legacy = '', ''
|
|
try:
|
|
identity = json.loads(a.package.read_text()).get('engine', '') if a.package else ''
|
|
except (OSError, ValueError):
|
|
pass
|
|
try:
|
|
gpu_id = (a.out / 'gpu-cache.id').read_text().strip()
|
|
if gpu_id:
|
|
identity = 'gpu ' + gpu_id
|
|
old = (a.out / 'gpu-cache.legacy.id').read_text().strip()
|
|
if old:
|
|
legacy = 'gpu ' + old
|
|
except OSError:
|
|
pass
|
|
reset_stale(a.cache, identity, legacy, lab)
|
|
if a.game:
|
|
seed(a.cache, a.engine / 'shader-catalog', a.game, a.out / 'eboot.elf', lab)
|