"""Apply a stock RetroArch slang preset to a still via librashader-cli. Does not patch shaders. Does not invent PixelSize. Your ShaderGlass .sgp files are the look reference; this only runs the same .slangp with the listed params. Manual look you are matching: 1. Nearest-upscale the orig (Affinity zoom). 300% => 3x. 2. Overlay ShaderGlass with the .sgp (Cyclon PixelSize 6, Lottes PixelSize 4). 3. Snapshot, then crop. This script can do step 1 (ffmpeg neighbor) and a stock slang render of the result. It cannot reproduce ShaderGlass window capture. If the plate is wrong, do it in ShaderGlass and drop the PNG next to the orig as *_crt.png. Presets: lottes crt-lottes-multipass.slangp LOTTES_CODE_(LOTTES_MULTIPASS 2X) V2.sgp cyclon crt-Cyclon.slangp LOTTES_CRT_CODE_(CYCLON 6X).sgp auto: cyclon if max(w,h) <= --auto-max (default 720), else lottes. """ from __future__ import annotations import argparse import json import shutil import subprocess import sys from pathlib import Path LOTTES_SGP: Path = Path( r"C:\Users\Ed\Downloads\ShaderGlass-1.2.2-win-x64" r"\LOTTES_CODE_(LOTTES_MULTIPASS 2X) V2.sgp" ) CYCLON_SGP: Path = Path( r"C:\Users\Ed\Downloads\ShaderGlass-1.2.2-win-x64" r"\LOTTES_CRT_CODE_(CYCLON 6X).sgp" ) SLANG_LOTTES: Path = Path( r"C:\Users\Ed\scoop\apps\retroarch\1.22.2\shaders" r"\shaders_slang\crt\crt-lottes-multipass.slangp" ) SLANG_CYCLON: Path = Path( r"C:\Users\Ed\scoop\apps\retroarch\1.22.2\shaders" r"\shaders_slang\crt\crt-Cyclon.slangp" ) CLI_DEFAULT: Path = Path(r"C:\Users\Ed\scoop\apps\librashader\0.12.0\librashader-cli.exe") # Knobs copied from the .sgp files. Not a substitute for PixelSize. LOTTES_PARAMS: str = ( "hardScan=-19,hardPix=-7,warpX=0,warpY=0,maskDark=1.3,maskLight=1.3," "shadowMask=4,brightBoost=1.15,bloomAmount=0,shape=1.9" ) CYCLON_PARAMS: str = ( "SCANLINE=0.2,INTERLACE=0,M_TYPE=0,SLOT=0,SLOTW=2,Maskl=0.55,Maskh=1," "bzl=0,ambient=0,REFLECT=0,WARPX=0,WARPY=0,BR_DEP=0,BRIGHTNESS_=1.05," "BLACK=0.01,vig=0" ) def probe_size(path: Path) -> tuple[int, int]: """Return ``(width, height)`` of an image or video via ``ffprobe``.""" raw = subprocess.check_output( [ "ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "json", str(path), ], text=True, ) stream = json.loads(raw)["streams"][0] return int(stream["width"]), int(stream["height"]) def pick_preset(width: int, height: int, name: str, auto_max: int) -> tuple[str, Path, Path, str]: """Resolve the preset name to ``(name, .sgp_path, .slangp_path, params)``; ``auto`` picks cyclon under ``auto_max``.""" if name == "auto": name = "cyclon" if max(width, height) <= auto_max else "lottes" if name == "lottes": return name, LOTTES_SGP, SLANG_LOTTES, LOTTES_PARAMS if name == "cyclon": return name, CYCLON_SGP, SLANG_CYCLON, CYCLON_PARAMS raise SystemExit(f"unknown preset {name}") def find_cli(explicit: Path | None) -> Path: """Locate ``librashader-cli``: explicit path, then PATH, then the scoop-installed default.""" if explicit is not None: if not explicit.is_file(): raise SystemExit(f"missing --cli {explicit}") return explicit which = shutil.which("librashader-cli") if which: return Path(which) if CLI_DEFAULT.is_file(): return CLI_DEFAULT raise SystemExit( "librashader-cli not on PATH. Install or pass --cli. " "Stock binary lives at " r"C:\Users\Ed\scoop\apps\librashader\0.12.0\librashader-cli.exe" ) def parse_crop(spec: str) -> str | None: """Parse a ``--crop`` value into an ffmpeg ``crop=...`` filter, or ``None`` to skip cropping.""" if spec in ("none", "", "off"): return None if spec == "auto": return "auto" parts = [int(p) for p in spec.replace(" ", "").split(",")] if len(parts) != 4: raise SystemExit("--crop wants auto, none, or L,T,R,B") left, top, right, bottom = parts return f"crop=iw-{left + right}:ih-{top + bottom}:{left}:{top}" def detect_crop(path: Path) -> str | None: """Run ``ffmpeg cropdetect`` on ``path`` and return the auto-detected crop filter (or ``None``).""" proc = subprocess.run( [ "ffmpeg", "-hide_banner", "-i", str(path), "-vf", "cropdetect=limit=24:round=2:reset=0", "-frames:v", "1", "-f", "null", "-", ], capture_output=True, text=True, ) crop = None for line in (proc.stderr or "").splitlines(): if "crop=" in line: crop = line.rsplit("crop=", 1)[-1].strip() return f"crop={crop}" if crop else None def run_ffmpeg(src: Path, dest: Path, vf: str) -> None: """Run ``ffmpeg -y`` with the given ``-vf`` filter to render a single still ``src`` -> ``dest``.""" dest.parent.mkdir(parents=True, exist_ok=True) subprocess.check_call( [ "ffmpeg", "-y", "-hide_banner", "-loglevel", "error", "-i", str(src), "-vf", vf, "-frames:v", "1", "-update", "1", str(dest), ] ) def run_librashader( cli: Path, slang: Path, src: Path, dest: Path, *, runtime: str, dimensions: str, params: str, ) -> None: """Render ``src`` through ``slang`` via the ``librashader-cli`` binary into ``dest``.""" dest.parent.mkdir(parents=True, exist_ok=True) dest.parent.mkdir(parents=True, exist_ok=True) cmd = [ str(cli), "render", "-r", runtime, "-d", dimensions, "--params", params, "-p", str(slang), "-i", str(src), "-o", str(dest), ] print(" ".join(cmd)) subprocess.check_call(cmd) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--in", dest="src", type=Path, required=True) parser.add_argument("--out", dest="dest", type=Path, required=True) parser.add_argument("--preset", choices=("auto", "lottes", "cyclon"), default="auto") parser.add_argument( "--nn", type=int, default=1, help="nearest integer pre-scale (Affinity zoom). 1 = none, 3 = 300%%", ) parser.add_argument( "--shader-scale", default="100%", help="librashader -d on the (possibly nn-scaled) image. 100%%, 300%%, 600%%, or WIDTHxHEIGHT", ) parser.add_argument("--runtime", default="d3d11") parser.add_argument("--auto-max", type=int, default=720) parser.add_argument("--crop", default="none", help="auto | none | L,T,R,B") parser.add_argument("--cli", type=Path, default=None) parser.add_argument("--dry-run", action="store_true") parser.add_argument( "--shaderglass", action="store_true", help="launch ShaderGlass with the matching .sgp (manual snapshot)", ) parser.add_argument( "--nn-only", action="store_true", help="only do the neighbor pre-scale; do not call librashader", ) args = parser.parse_args(argv) if not args.src.is_file(): raise SystemExit(f"missing {args.src}") if args.nn < 1: raise SystemExit("--nn must be >= 1") width, height = probe_size(args.src) name, sgp, slang, params = pick_preset(width, height, args.preset, args.auto_max) print(f"size {width}x{height} preset={name} nn={args.nn} shader-scale={args.shader_scale}") print(f"sgp {sgp}") print(f"slang {slang}") work = args.src tmp_nn: Path | None = None if args.nn > 1: tmp_nn = args.dest.with_name(args.dest.stem + f"_nn{args.nn}.png") vf = f"scale=iw*{args.nn}:ih*{args.nn}:flags=neighbor,format=rgba" print("ffmpeg", "-i", work, "-vf", vf, tmp_nn) if not args.dry_run: run_ffmpeg(work, tmp_nn, vf) work = tmp_nn if args.nn_only: if tmp_nn is None: raise SystemExit("--nn-only needs --nn > 1") if not args.dry_run: shutil.copy2(tmp_nn, args.dest) print(f"wrote {args.dest} (nn only)") return 0 cli = None if args.dry_run else find_cli(args.cli) if args.dry_run: print( "librashader-cli render", "-r", args.runtime, "-d", args.shader_scale, "--params", params, "-p", slang, "-i", work, "-o", args.dest, ) else: assert cli is not None if not slang.is_file(): raise SystemExit(f"missing slang {slang}") run_librashader( cli, slang, work, args.dest, runtime=args.runtime, dimensions=args.shader_scale, params=params, ) crop_spec = parse_crop(args.crop) if crop_spec == "auto" and not args.dry_run: crop_filter = detect_crop(args.dest) if crop_filter: cropped = args.dest.with_name(args.dest.stem + "_crop.png") run_ffmpeg(args.dest, cropped, crop_filter) shutil.move(str(cropped), str(args.dest)) elif crop_spec and crop_spec != "auto" and not args.dry_run: cropped = args.dest.with_name(args.dest.stem + "_crop.png") run_ffmpeg(args.dest, cropped, crop_spec) shutil.move(str(cropped), str(args.dest)) print(f"wrote {args.dest}") if args.shaderglass: exe = shutil.which("shaderglass") or shutil.which("ShaderGlass") if not exe: raise SystemExit("shaderglass not on PATH") subprocess.Popen([exe, str(sgp)]) return 0 if __name__ == "__main__": raise SystemExit(main())